diff --git a/data/Dockerfiles/bootstrap/main.py b/data/Dockerfiles/bootstrap/main.py index 6b10998ca..8b2317049 100644 --- a/data/Dockerfiles/bootstrap/main.py +++ b/data/Dockerfiles/bootstrap/main.py @@ -1,7 +1,14 @@ import os import sys +import signal + +def handle_sigterm(signum, frame): + print("Received SIGTERM, exiting gracefully...") + sys.exit(0) def main(): + signal.signal(signal.SIGTERM, handle_sigterm) + container_name = os.getenv("CONTAINER_NAME") if container_name == "sogo-mailcow": @@ -12,6 +19,8 @@ def main(): from modules.BootstrapPostfix import Bootstrap elif container_name == "dovecot-mailcow": from modules.BootstrapDovecot import Bootstrap + elif container_name == "rspamd-mailcow": + from modules.BootstrapRspamd import Bootstrap else: print(f"No bootstrap handler for container: {container_name}", file=sys.stderr) sys.exit(1) diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapBase.py b/data/Dockerfiles/bootstrap/modules/BootstrapBase.py index 46de259d1..1af5a3549 100644 --- a/data/Dockerfiles/bootstrap/modules/BootstrapBase.py +++ b/data/Dockerfiles/bootstrap/modules/BootstrapBase.py @@ -13,6 +13,7 @@ import redis import hashlib import json from pathlib import Path +import dns.resolver import mysql.connector from jinja2 import Environment, FileSystemLoader @@ -395,6 +396,29 @@ class BootstrapBase: result = sock.connect_ex((host, port)) return result == 0 + def resolve_docker_dns_record(self, hostname, record_type="A"): + """ + Resolves DNS A or AAAA records for a given hostname. + + Args: + hostname (str): The domain to query. + record_type (str): "A" for IPv4, "AAAA" for IPv6. Default is "A". + + Returns: + list[str]: A list of resolved IP addresses. + + Raises: + Exception: If resolution fails or no results are found. + """ + + try: + resolver = dns.resolver.Resolver() + resolver.nameservers = ["127.0.0.11"] + answers = resolver.resolve(hostname, record_type) + return [answer.to_text() for answer in answers] + except Exception as e: + raise Exception(f"Failed to resolve {record_type} record for {hostname}: {e}") + def kill_proc(self, process): """ Sends a SIGTERM signal to all processes matching the given name using `killall`. diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapRspamd.py b/data/Dockerfiles/bootstrap/modules/BootstrapRspamd.py new file mode 100644 index 000000000..908b10ef7 --- /dev/null +++ b/data/Dockerfiles/bootstrap/modules/BootstrapRspamd.py @@ -0,0 +1,137 @@ +from jinja2 import Environment, FileSystemLoader +from modules.BootstrapBase import BootstrapBase +from pathlib import Path +import os +import sys +import time +import platform + +class Bootstrap(BootstrapBase): + def bootstrap(self): + # Connect to MySQL + self.connect_mysql() + + # Connect to MySQL + self.connect_redis() + + # get dovecot ips + dovecot_v4 = [] + dovecot_v6 = [] + while not dovecot_v4 and not dovecot_v6: + try: + dovecot_v4 = self.resolve_docker_dns_record("dovecot-mailcow", "A") + dovecot_v6 = self.resolve_docker_dns_record("dovecot-mailcow", "AAAA") + except Exception as e: + print(e) + if not dovecot_v4 and not dovecot_v6: + print("Waiting for Dovecot IPs...") + time.sleep(3) + + # get rspamd ips + rspamd_v4 = [] + rspamd_v6 = [] + while not rspamd_v4 and not rspamd_v6: + try: + rspamd_v4 = self.resolve_docker_dns_record("rspamd-mailcow", "A") + rspamd_v6 = self.resolve_docker_dns_record("rspamd-mailcow", "AAAA") + except Exception: + print(e) + if not rspamd_v4 and not rspamd_v6: + print("Waiting for Rspamd IPs...") + time.sleep(3) + + # wait for Services + services = [ + ["php-fpm-mailcow", 9001], + ["php-fpm-mailcow", 9002] + ] + for service in services: + while not self.is_port_open(service[0], service[1]): + print(f"Waiting for {service[0]} on port {service[1]}...") + time.sleep(1) + print(f"Service {service[0]} on port {service[1]} is ready!") + + for dir_path in ["/etc/rspamd/plugins.d", "/etc/rspamd/custom"]: + Path(dir_path).mkdir(parents=True, exist_ok=True) + for file_path in ["/etc/rspamd/rspamd.conf.local", "/etc/rspamd/rspamd.conf.override"]: + Path(file_path).touch(exist_ok=True) + self.set_permissions("/var/lib/rspamd", 0o755) + + + # Setup Jinja2 Environment and load vars + self.env = Environment( + loader=FileSystemLoader('./etc/rspamd/config_templates'), + keep_trailing_newline=True, + lstrip_blocks=True, + trim_blocks=True + ) + extra_vars = { + "DOVECOT_V4": dovecot_v4[0], + "DOVECOT_V6": dovecot_v6[0], + "RSPAMD_V4": rspamd_v4[0], + "RSPAMD_V6": rspamd_v6[0], + } + self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars) + + print("Set Timezone") + self.set_timezone() + + print("Render config") + self.render_config("mailcow_networks.map.j2", "/etc/rspamd/custom/mailcow_networks.map") + self.render_config("dovecot_trusted.map.j2", "/etc/rspamd/custom/dovecot_trusted.map") + self.render_config("rspamd_trusted.map.j2", "/etc/rspamd/custom/rspamd_trusted.map") + self.render_config("external_services.conf.j2", "/etc/rspamd/local.d/external_services.conf") + self.render_config("redis.conf.j2", "/etc/rspamd/local.d/redis.conf") + self.render_config("dqs-rbl.conf.j2", "/etc/rspamd/custom/dqs-rbl.conf") + self.render_config("worker-controller-password.inc.j2", "/etc/rspamd/override.d/worker-controller-password.inc") + + # Fix missing default global maps, if any + # These exists in mailcow UI and should not be removed + files = [ + "/etc/rspamd/custom/global_mime_from_blacklist.map", + "/etc/rspamd/custom/global_rcpt_blacklist.map", + "/etc/rspamd/custom/global_smtp_from_blacklist.map", + "/etc/rspamd/custom/global_mime_from_whitelist.map", + "/etc/rspamd/custom/global_rcpt_whitelist.map", + "/etc/rspamd/custom/global_smtp_from_whitelist.map", + "/etc/rspamd/custom/bad_languages.map", + "/etc/rspamd/custom/sa-rules", + "/etc/rspamd/custom/dovecot_trusted.map", + "/etc/rspamd/custom/rspamd_trusted.map", + "/etc/rspamd/custom/mailcow_networks.map", + "/etc/rspamd/custom/ip_wl.map", + "/etc/rspamd/custom/fishy_tlds.map", + "/etc/rspamd/custom/bad_words.map", + "/etc/rspamd/custom/bad_asn.map", + "/etc/rspamd/custom/bad_words_de.map", + "/etc/rspamd/custom/bulk_header.map", + "/etc/rspamd/custom/bad_header.map" + ] + for file in files: + path = Path(file) + path.parent.mkdir(parents=True, exist_ok=True) + path.touch(exist_ok=True) + + # Fix permissions + paths_rspamd = [ + "/var/lib/rspamd", + "/etc/rspamd/local.d", + "/etc/rspamd/override.d", + "/etc/rspamd/rspamd.conf.local", + "/etc/rspamd/rspamd.conf.override", + "/etc/rspamd/plugins.d" + ] + for path in paths_rspamd: + self.set_owner(path, "_rspamd", "_rspamd", recursive=True) + self.set_owner("/etc/rspamd/custom", "_rspamd", "_rspamd") + self.set_permissions("/etc/rspamd/custom", 0o755) + + custom_path = Path("/etc/rspamd/custom") + for child in custom_path.iterdir(): + if child.is_file(): + self.set_owner(child, 82, 82) + self.set_permissions(child, 0o644) + + # Provide additional lua modules + arch = platform.machine() + self.run_command(["ln", "-s", f"/usr/lib/{arch}-linux-gnu/liblua5.1-cjson.so.0.0.0", "/usr/lib/rspamd/cjson.so"], check=False) diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 4037fc725..e4ca40b32 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -118,7 +118,8 @@ RUN addgroup -g 5000 vmail \ RUN pip install --break-system-packages \ mysql-connector-python \ jinja2 \ - redis + redis \ + dnspython COPY data/Dockerfiles/bootstrap /bootstrap diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index b857cbf38..f0100f9a6 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -43,7 +43,8 @@ RUN groupadd -g 102 postfix \ RUN pip install --break-system-packages \ mysql-connector-python \ jinja2 \ - redis + redis \ + dnspython COPY data/Dockerfiles/bootstrap /bootstrap COPY data/Dockerfiles/postfix/supervisord.conf /etc/supervisor/supervisord.conf diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 68b38d3a7..153aa3966 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -14,10 +14,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ dnsutils \ netcat-traditional \ wget \ - redis-tools \ - procps \ + redis-tools \ + procps \ nano \ lua-cjson \ + python3 python3-pip \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ && wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\ && apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \ @@ -29,12 +30,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && echo 'alias ll="ls -la --color"' >> ~/.bashrc \ && sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua -COPY settings.conf /etc/rspamd/settings.conf -COPY set_worker_password.sh /set_worker_password.sh -COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN pip install --break-system-packages \ + mysql-connector-python \ + jinja2 \ + redis \ + dnspython + + +COPY data/Dockerfiles/bootstrap /bootstrap +COPY data/Dockerfiles/rspamd/settings.conf /etc/rspamd/settings.conf +COPY data/Dockerfiles/rspamd/set_worker_password.sh /set_worker_password.sh +COPY data/Dockerfiles/rspamd/docker-entrypoint.sh /docker-entrypoint.sh -ENTRYPOINT ["/docker-entrypoint.sh"] STOPSIGNAL SIGTERM - +ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["/usr/bin/rspamd", "-f", "-u", "_rspamd", "-g", "_rspamd"] diff --git a/data/Dockerfiles/rspamd/docker-entrypoint.sh b/data/Dockerfiles/rspamd/docker-entrypoint.sh index 7385488b0..bf25a98fb 100755 --- a/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -1,144 +1,5 @@ #!/bin/bash -until nc phpfpm 9001 -z; do - echo "Waiting for PHP on port 9001..." - sleep 3 -done - -until nc phpfpm 9002 -z; do - echo "Waiting for PHP on port 9002..." - sleep 3 -done - -mkdir -p /etc/rspamd/plugins.d \ - /etc/rspamd/custom - -touch /etc/rspamd/rspamd.conf.local \ - /etc/rspamd/rspamd.conf.override - -chmod 755 /var/lib/rspamd - - -[[ ! -f /etc/rspamd/override.d/worker-controller-password.inc ]] && echo '# Autogenerated by mailcow' > /etc/rspamd/override.d/worker-controller-password.inc - -echo ${IPV4_NETWORK}.0/24 > /etc/rspamd/custom/mailcow_networks.map -echo ${IPV6_NETWORK} >> /etc/rspamd/custom/mailcow_networks.map - -DOVECOT_V4= -DOVECOT_V6= -until [[ ! -z ${DOVECOT_V4} ]]; do - DOVECOT_V4=$(dig a dovecot +short) - DOVECOT_V6=$(dig aaaa dovecot +short) - [[ ! -z ${DOVECOT_V4} ]] && break; - echo "Waiting for Dovecot..." - sleep 3 -done -echo ${DOVECOT_V4}/32 > /etc/rspamd/custom/dovecot_trusted.map -if [[ ! -z ${DOVECOT_V6} ]]; then - echo ${DOVECOT_V6}/128 >> /etc/rspamd/custom/dovecot_trusted.map -fi - -RSPAMD_V4= -RSPAMD_V6= -until [[ ! -z ${RSPAMD_V4} ]]; do - RSPAMD_V4=$(dig a rspamd +short) - RSPAMD_V6=$(dig aaaa rspamd +short) - [[ ! -z ${RSPAMD_V4} ]] && break; - echo "Waiting for Rspamd..." - sleep 3 -done -echo ${RSPAMD_V4}/32 > /etc/rspamd/custom/rspamd_trusted.map -if [[ ! -z ${RSPAMD_V6} ]]; then - echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map -fi - -if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then - cat < /etc/rspamd/local.d/redis.conf -read_servers = "redis:6379"; -write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}"; -password = "${REDISPASS}"; -timeout = 10; -EOF - until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do - echo "Waiting for Redis @redis-mailcow..." - sleep 2 - done - until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do - echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..." - sleep 2 - done - redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT} -else - cat < /etc/rspamd/local.d/redis.conf -servers = "redis:6379"; -password = "${REDISPASS}"; -timeout = 10; -EOF - until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do - echo "Waiting for Redis slave..." - sleep 2 - done - redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE -fi - -if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - if [[ -f /etc/rspamd/local.d/external_services.conf ]]; then - rm /etc/rspamd/local.d/external_services.conf - fi -else - cat < /etc/rspamd/local.d/external_services.conf -oletools { - # default olefy settings - servers = "olefy:10055"; - # needs to be set explicitly for Rspamd < 1.9.5 - scan_mime_parts = true; - # mime-part regex matching in content-type or filename - # block all macros - extended = true; - max_size = 3145728; - timeout = 20.0; - retransmits = 1; -} -EOF -fi - -# Provide additional lua modules -ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so - -chown -R _rspamd:_rspamd /var/lib/rspamd \ - /etc/rspamd/local.d \ - /etc/rspamd/override.d \ - /etc/rspamd/rspamd.conf.local \ - /etc/rspamd/rspamd.conf.override \ - /etc/rspamd/plugins.d - -# Fix missing default global maps, if any -# These exists in mailcow UI and should not be removed -touch /etc/rspamd/custom/global_mime_from_blacklist.map \ - /etc/rspamd/custom/global_rcpt_blacklist.map \ - /etc/rspamd/custom/global_smtp_from_blacklist.map \ - /etc/rspamd/custom/global_mime_from_whitelist.map \ - /etc/rspamd/custom/global_rcpt_whitelist.map \ - /etc/rspamd/custom/global_smtp_from_whitelist.map \ - /etc/rspamd/custom/bad_languages.map \ - /etc/rspamd/custom/sa-rules \ - /etc/rspamd/custom/dovecot_trusted.map \ - /etc/rspamd/custom/rspamd_trusted.map \ - /etc/rspamd/custom/mailcow_networks.map \ - /etc/rspamd/custom/ip_wl.map \ - /etc/rspamd/custom/fishy_tlds.map \ - /etc/rspamd/custom/bad_words.map \ - /etc/rspamd/custom/bad_asn.map \ - /etc/rspamd/custom/bad_words_de.map \ - /etc/rspamd/custom/bulk_header.map \ - /etc/rspamd/custom/bad_header.map - -# www-data (82) group needs to write to these files -chown _rspamd:_rspamd /etc/rspamd/custom/ -chmod 0755 /etc/rspamd/custom/. -chown -R 82:82 /etc/rspamd/custom/* -chmod 644 -R /etc/rspamd/custom/* - # Run hooks for file in /hooks/*; do if [ -x "${file}" ]; then @@ -147,190 +8,13 @@ for file in /hooks/*; do fi done -# If DQS KEY is set in mailcow.conf add Spamhaus DQS RBLs -if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then - cat < /etc/rspamd/custom/dqs-rbl.conf - # Autogenerated by mailcow. DO NOT TOUCH! - spamhaus { - rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; - from = false; - } - spamhaus_from { - from = true; - received = false; - rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; - returncodes { - SPAMHAUS_ZEN = [ "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.9", "127.0.0.10", "127.0.0.11" ]; - } - } - spamhaus_authbl_received { - # Check if the sender client is listed in AuthBL (AuthBL is *not* part of ZEN) - rbl = "${SPAMHAUS_DQS_KEY}.authbl.dq.spamhaus.net"; - from = false; - received = true; - ipv6 = true; - returncodes { - SH_AUTHBL_RECEIVED = "127.0.0.20" - } - } - spamhaus_dbl { - # Add checks on the HELO string - rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; - helo = true; - rdns = true; - dkim = true; - disable_monitoring = true; - returncodes { - RBL_DBL_SPAM = "127.0.1.2"; - RBL_DBL_PHISH = "127.0.1.4"; - RBL_DBL_MALWARE = "127.0.1.5"; - RBL_DBL_BOTNET = "127.0.1.6"; - RBL_DBL_ABUSED_SPAM = "127.0.1.102"; - RBL_DBL_ABUSED_PHISH = "127.0.1.104"; - RBL_DBL_ABUSED_MALWARE = "127.0.1.105"; - RBL_DBL_ABUSED_BOTNET = "127.0.1.106"; - RBL_DBL_DONT_QUERY_IPS = "127.0.1.255"; - } - } - spamhaus_dbl_fullurls { - ignore_defaults = true; - no_ip = true; - rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; - selector = 'urls:get_host' - disable_monitoring = true; - returncodes { - DBLABUSED_SPAM_FULLURLS = "127.0.1.102"; - DBLABUSED_PHISH_FULLURLS = "127.0.1.104"; - DBLABUSED_MALWARE_FULLURLS = "127.0.1.105"; - DBLABUSED_BOTNET_FULLURLS = "127.0.1.106"; - } - } - spamhaus_zrd { - # Add checks on the HELO string also for DQS - rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; - helo = true; - rdns = true; - dkim = true; - disable_monitoring = true; - returncodes { - RBL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; - RBL_ZRD_FRESH_DOMAIN = [ - "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" - ]; - RBL_ZRD_DONT_QUERY_IPS = "127.0.2.255"; - } - } - "SPAMHAUS_ZEN_URIBL" { - enabled = true; - rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net"; - resolve_ip = true; - checks = ['urls']; - replyto = true; - emails = true; - ipv4 = true; - ipv6 = true; - emails_domainonly = true; - returncodes { - URIBL_SBL = "127.0.0.2"; - URIBL_SBL_CSS = "127.0.0.3"; - URIBL_XBL = ["127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7"]; - URIBL_PBL = ["127.0.0.10", "127.0.0.11"]; - URIBL_DROP = "127.0.0.9"; - } - } - SH_EMAIL_DBL { - ignore_defaults = true; - replyto = true; - emails_domainonly = true; - disable_monitoring = true; - rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; - returncodes = { - SH_EMAIL_DBL = [ - "127.0.1.2", - "127.0.1.4", - "127.0.1.5", - "127.0.1.6" - ]; - SH_EMAIL_DBL_ABUSED = [ - "127.0.1.102", - "127.0.1.104", - "127.0.1.105", - "127.0.1.106" - ]; - SH_EMAIL_DBL_DONT_QUERY_IPS = [ "127.0.1.255" ]; - } - } - SH_EMAIL_ZRD { - ignore_defaults = true; - replyto = true; - emails_domainonly = true; - disable_monitoring = true; - rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; - returncodes = { - SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; - SH_EMAIL_ZRD_FRESH_DOMAIN = [ - "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" - ]; - SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ]; - } - } - "DBL" { - # override the defaults for DBL defined in modules.d/rbl.conf - rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net"; - disable_monitoring = true; - } - "ZRD" { - ignore_defaults = true; - rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net"; - no_ip = true; - dkim = true; - emails = true; - emails_domainonly = true; - urls = true; - returncodes = { - ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; - ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"]; - } - } - spamhaus_sbl_url { - ignore_defaults = true - rbl = "${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net"; - checks = ['urls']; - disable_monitoring = true; - returncodes { - SPAMHAUS_SBL_URL = "127.0.0.2"; - } - } +python3 -u /bootstrap/main.py +BOOTSTRAP_EXIT_CODE=$? - SH_HBL_EMAIL { - ignore_defaults = true; - rbl = "_email.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net"; - emails_domainonly = false; - selector = "from('smtp').lower;from('mime').lower"; - ignore_whitelist = true; - checks = ['emails', 'replyto']; - hash = "sha1"; - returncodes = { - SH_HBL_EMAIL = [ - "127.0.3.2" - ]; - } - } - - spamhaus_dqs_hbl { - symbol = "HBL_FILE_UNKNOWN"; - rbl = "_file.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net."; - selector = "attachments('rbase32', 'sha256')"; - ignore_whitelist = true; - ignore_defaults = true; - returncodes { - SH_HBL_FILE_MALICIOUS = "127.0.3.10"; - SH_HBL_FILE_SUSPICIOUS = "127.0.3.15"; - } - } -EOF -else - rm -rf /etc/rspamd/custom/dqs-rbl.conf +if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then + echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Rspamd." + exit $BOOTSTRAP_EXIT_CODE fi +echo "Bootstrap succeeded. Starting Rspamd..." exec "$@" diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 2a383ec56..84255534e 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -46,7 +46,8 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ RUN pip install --break-system-packages \ mysql-connector-python \ jinja2 \ - redis + redis \ + dnspython COPY data/Dockerfiles/bootstrap /bootstrap diff --git a/data/conf/rspamd/config_templates/dovecot_trusted.map.j2 b/data/conf/rspamd/config_templates/dovecot_trusted.map.j2 new file mode 100644 index 000000000..4aef1f7ba --- /dev/null +++ b/data/conf/rspamd/config_templates/dovecot_trusted.map.j2 @@ -0,0 +1,2 @@ +{{ DOVECOT_V4 }}/32 +{{ DOVECOT_V6 }}/128 \ No newline at end of file diff --git a/data/conf/rspamd/config_templates/dqs-rbl.conf.j2 b/data/conf/rspamd/config_templates/dqs-rbl.conf.j2 new file mode 100644 index 000000000..b8e75b61f --- /dev/null +++ b/data/conf/rspamd/config_templates/dqs-rbl.conf.j2 @@ -0,0 +1,179 @@ +{% if SPAMHAUS_DQS_KEY %} +spamhaus { + rbl = "{{ SPAMHAUS_DQS_KEY }}.zen.dq.spamhaus.net"; + from = false; +} +spamhaus_from { + from = true; + received = false; + rbl = "{{ SPAMHAUS_DQS_KEY }}.zen.dq.spamhaus.net"; + returncodes { + SPAMHAUS_ZEN = [ "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.9", "127.0.0.10", "127.0.0.11" ]; + } +} +spamhaus_authbl_received { + # Check if the sender client is listed in AuthBL (AuthBL is *not* part of ZEN) + rbl = "{{ SPAMHAUS_DQS_KEY }}.authbl.dq.spamhaus.net"; + from = false; + received = true; + ipv6 = true; + returncodes { + SH_AUTHBL_RECEIVED = "127.0.0.20" + } +} +spamhaus_dbl { + # Add checks on the HELO string + rbl = "{{ SPAMHAUS_DQS_KEY }}.dbl.dq.spamhaus.net"; + helo = true; + rdns = true; + dkim = true; + disable_monitoring = true; + returncodes { + RBL_DBL_SPAM = "127.0.1.2"; + RBL_DBL_PHISH = "127.0.1.4"; + RBL_DBL_MALWARE = "127.0.1.5"; + RBL_DBL_BOTNET = "127.0.1.6"; + RBL_DBL_ABUSED_SPAM = "127.0.1.102"; + RBL_DBL_ABUSED_PHISH = "127.0.1.104"; + RBL_DBL_ABUSED_MALWARE = "127.0.1.105"; + RBL_DBL_ABUSED_BOTNET = "127.0.1.106"; + RBL_DBL_DONT_QUERY_IPS = "127.0.1.255"; + } +} +spamhaus_dbl_fullurls { + ignore_defaults = true; + no_ip = true; + rbl = "{{ SPAMHAUS_DQS_KEY }}.dbl.dq.spamhaus.net"; + selector = 'urls:get_host' + disable_monitoring = true; + returncodes { + DBLABUSED_SPAM_FULLURLS = "127.0.1.102"; + DBLABUSED_PHISH_FULLURLS = "127.0.1.104"; + DBLABUSED_MALWARE_FULLURLS = "127.0.1.105"; + DBLABUSED_BOTNET_FULLURLS = "127.0.1.106"; + } +} +spamhaus_zrd { + # Add checks on the HELO string also for DQS + rbl = "{{ SPAMHAUS_DQS_KEY }}.zrd.dq.spamhaus.net"; + helo = true; + rdns = true; + dkim = true; + disable_monitoring = true; + returncodes { + RBL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + RBL_ZRD_FRESH_DOMAIN = [ + "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" + ]; + RBL_ZRD_DONT_QUERY_IPS = "127.0.2.255"; + } +} +"SPAMHAUS_ZEN_URIBL" { + enabled = true; + rbl = "{{ SPAMHAUS_DQS_KEY }}.zen.dq.spamhaus.net"; + resolve_ip = true; + checks = ['urls']; + replyto = true; + emails = true; + ipv4 = true; + ipv6 = true; + emails_domainonly = true; + returncodes { + URIBL_SBL = "127.0.0.2"; + URIBL_SBL_CSS = "127.0.0.3"; + URIBL_XBL = ["127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7"]; + URIBL_PBL = ["127.0.0.10", "127.0.0.11"]; + URIBL_DROP = "127.0.0.9"; + } +} +SH_EMAIL_DBL { + ignore_defaults = true; + replyto = true; + emails_domainonly = true; + disable_monitoring = true; + rbl = "{{ SPAMHAUS_DQS_KEY }}.dbl.dq.spamhaus.net"; + returncodes = { + SH_EMAIL_DBL = [ + "127.0.1.2", + "127.0.1.4", + "127.0.1.5", + "127.0.1.6" + ]; + SH_EMAIL_DBL_ABUSED = [ + "127.0.1.102", + "127.0.1.104", + "127.0.1.105", + "127.0.1.106" + ]; + SH_EMAIL_DBL_DONT_QUERY_IPS = [ "127.0.1.255" ]; + } +} +SH_EMAIL_ZRD { + ignore_defaults = true; + replyto = true; + emails_domainonly = true; + disable_monitoring = true; + rbl = "{{ SPAMHAUS_DQS_KEY }}.zrd.dq.spamhaus.net"; + returncodes = { + SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + SH_EMAIL_ZRD_FRESH_DOMAIN = [ + "127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24" + ]; + SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ]; + } +} +"DBL" { + # override the defaults for DBL defined in modules.d/rbl.conf + rbl = "{{ SPAMHAUS_DQS_KEY }}.dbl.dq.spamhaus.net"; + disable_monitoring = true; +} +"ZRD" { + ignore_defaults = true; + rbl = "{{ SPAMHAUS_DQS_KEY }}.zrd.dq.spamhaus.net"; + no_ip = true; + dkim = true; + emails = true; + emails_domainonly = true; + urls = true; + returncodes = { + ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"]; + ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"]; + } +} +spamhaus_sbl_url { + ignore_defaults = true + rbl = "{{ SPAMHAUS_DQS_KEY }}.sbl.dq.spamhaus.net"; + checks = ['urls']; + disable_monitoring = true; + returncodes { + SPAMHAUS_SBL_URL = "127.0.0.2"; + } +} + +SH_HBL_EMAIL { + ignore_defaults = true; + rbl = "_email.{{ SPAMHAUS_DQS_KEY }}.hbl.dq.spamhaus.net"; + emails_domainonly = false; + selector = "from('smtp').lower;from('mime').lower"; + ignore_whitelist = true; + checks = ['emails', 'replyto']; + hash = "sha1"; + returncodes = { + SH_HBL_EMAIL = [ + "127.0.3.2" + ]; + } +} + +spamhaus_dqs_hbl { + symbol = "HBL_FILE_UNKNOWN"; + rbl = "_file.{{ SPAMHAUS_DQS_KEY }}.hbl.dq.spamhaus.net."; + selector = "attachments('rbase32', 'sha256')"; + ignore_whitelist = true; + ignore_defaults = true; + returncodes { + SH_HBL_FILE_MALICIOUS = "127.0.3.10"; + SH_HBL_FILE_SUSPICIOUS = "127.0.3.15"; + } +} +{% endif %} diff --git a/data/conf/rspamd/config_templates/external_services.conf.j2 b/data/conf/rspamd/config_templates/external_services.conf.j2 new file mode 100644 index 000000000..d1777cedc --- /dev/null +++ b/data/conf/rspamd/config_templates/external_services.conf.j2 @@ -0,0 +1,16 @@ +{% if SKIP_OLEFY|lower in ['y', 'yes'] %} +# OLEFY deactivated +{% else %} +oletools { + # default olefy settings + servers = "olefy-mailcow:10055"; + # needs to be set explicitly for Rspamd < 1.9.5 + scan_mime_parts = true; + # mime-part regex matching in content-type or filename + # block all macros + extended = true; + max_size = 3145728; + timeout = 20.0; + retransmits = 1; +} +{% endif %} \ No newline at end of file diff --git a/data/conf/rspamd/config_templates/mailcow_networks.map.j2 b/data/conf/rspamd/config_templates/mailcow_networks.map.j2 new file mode 100644 index 000000000..6ede0cc81 --- /dev/null +++ b/data/conf/rspamd/config_templates/mailcow_networks.map.j2 @@ -0,0 +1,2 @@ +{{ IPV4_NETWORK }}.0/24 +{{ IPV6_NETWORK }} \ No newline at end of file diff --git a/data/conf/rspamd/config_templates/redis.conf.j2 b/data/conf/rspamd/config_templates/redis.conf.j2 new file mode 100644 index 000000000..2024ad4a0 --- /dev/null +++ b/data/conf/rspamd/config_templates/redis.conf.j2 @@ -0,0 +1,10 @@ +{% if REDIS_SLAVEOF_IP and REDIS_SLAVEOF_PORT %} +read_servers = "redis-mailcow:6379"; +write_servers = "{{ REDIS_SLAVEOF_IP }}:{{ REDIS_SLAVEOF_PORT }}"; +password = "{{ REDISPASS }}"; +timeout = 10; +{% else %} +servers = "redis-mailcow:6379"; +password = "{{ REDISPASS }}"; +timeout = 10; +{% endif %} diff --git a/data/conf/rspamd/config_templates/rspamd_trusted.map.j2 b/data/conf/rspamd/config_templates/rspamd_trusted.map.j2 new file mode 100644 index 000000000..362480cb8 --- /dev/null +++ b/data/conf/rspamd/config_templates/rspamd_trusted.map.j2 @@ -0,0 +1,2 @@ +{{ RSPAMD_V4 }}/32 +{{ RSPAMD_V6 }}/128 \ No newline at end of file diff --git a/data/conf/rspamd/config_templates/worker-controller-password.inc.j2 b/data/conf/rspamd/config_templates/worker-controller-password.inc.j2 new file mode 100644 index 000000000..21db66a45 --- /dev/null +++ b/data/conf/rspamd/config_templates/worker-controller-password.inc.j2 @@ -0,0 +1 @@ +# Autogenerated by mailcow diff --git a/docker-compose.yml b/docker-compose.yml index 696820374..b315c6e35 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,12 +84,16 @@ services: - clamd rspamd-mailcow: - image: ghcr.io/mailcow/rspamd:2.2 + image: ghcr.io/mailcow/rspamd:nightly-19052025 stop_grace_period: 30s depends_on: - dovecot-mailcow - clamd-mailcow environment: + - CONTAINER_NAME=rspamd-mailcow + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} - TZ=${TZ} - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} @@ -99,6 +103,7 @@ services: - SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-} volumes: - ./data/hooks/rspamd:/hooks:Z + - ./data/conf/rspamd/config_templates/:/etc/rspamd/config_templates:z - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:Z - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:Z @@ -106,6 +111,7 @@ services: - ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro,Z - ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local:Z - ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override:Z + - mysql-socket-vol-1:/var/run/mysqld/ - rspamd-vol-1:/var/lib/rspamd restart: always hostname: rspamd