diff --git a/data/Dockerfiles/bootstrap/main.py b/data/Dockerfiles/bootstrap/main.py index 8b2317049..e4836a116 100644 --- a/data/Dockerfiles/bootstrap/main.py +++ b/data/Dockerfiles/bootstrap/main.py @@ -21,6 +21,8 @@ def main(): from modules.BootstrapDovecot import Bootstrap elif container_name == "rspamd-mailcow": from modules.BootstrapRspamd import Bootstrap + elif container_name == "clamd-mailcow": + from modules.BootstrapClamd 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 1af5a3549..2385aff3f 100644 --- a/data/Dockerfiles/bootstrap/modules/BootstrapBase.py +++ b/data/Dockerfiles/bootstrap/modules/BootstrapBase.py @@ -30,16 +30,14 @@ class BootstrapBase: self.mysql_conn = None self.redis_conn = None - def render_config(self, template_name, output_path): + def render_config(self, template_name, output_path, clean_blank_lines=False): """ Renders a Jinja2 template and writes it to the specified output path. - The method uses the class's `self.env` Jinja2 environment and `self.env_vars` - for rendering template variables. - Args: - template_name (str): Name of the template file. - output_path (str or Path): Path to write the rendered output file. + template_name (str): Name of the template file. + output_path (str or Path): Path to write the rendered output file. + clean_blank_lines (bool): If True, removes empty/whitespace-only lines from rendered output. """ output_path = Path(output_path) @@ -48,6 +46,12 @@ class BootstrapBase: template = self.env.get_template(template_name) rendered = template.render(self.env_vars) + if clean_blank_lines: + rendered = "\n".join(line for line in rendered.splitlines() if line.strip()) + + # converts output to Unix-style line endings + rendered = rendered.replace('\r\n', '\n').replace('\r', '\n') + with open(output_path, "w") as f: f.write(rendered) diff --git a/data/Dockerfiles/bootstrap/modules/BootstrapClamd.py b/data/Dockerfiles/bootstrap/modules/BootstrapClamd.py new file mode 100644 index 000000000..9ac9d5bf3 --- /dev/null +++ b/data/Dockerfiles/bootstrap/modules/BootstrapClamd.py @@ -0,0 +1,58 @@ +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): + # Skip Clamd if set + if self.isYes(os.getenv("SKIP_CLAMD", "")): + print("SKIP_CLAMD is set, skipping ClamAV startup...") + time.sleep(365 * 24 * 60 * 60) + sys.exit(1) + + # Connect to MySQL + self.connect_mysql() + + print("Cleaning up tmp files...") + tmp_files = Path("/var/lib/clamav").glob("clamav-*.tmp") + for tmp_file in tmp_files: + try: + self.remove(tmp_file) + print(f"Removed: {tmp_file}") + except Exception as e: + print(f"Failed to remove {tmp_file}: {e}") + + self.create_dir("/run/clamav") + self.create_dir("/var/lib/clamav") + + # Setup Jinja2 Environment and load vars + self.env = Environment( + loader=FileSystemLoader('./etc/clamav/config_templates'), + keep_trailing_newline=True, + lstrip_blocks=True, + trim_blocks=True + ) + extra_vars = { + } + self.env_vars = self.prepare_template_vars('/overwrites.json', extra_vars) + + print("Set Timezone") + self.set_timezone() + + print("Render config") + self.render_config("whitelist.ign2.j2", "/var/lib/clamav/whitelist.ign2", clean_blank_lines=True) + + # Fix permissions + self.set_owner("/var/lib/clamav", "clamav", "clamav", recursive=True) + self.set_owner("/run/clamav", "clamav", "clamav", recursive=True) + self.set_permissions("/var/lib/clamav", 0o755) + for item in Path("/var/lib/clamav").glob("*"): + self.set_permissions(item, 0o644) + self.set_permissions("/run/clamav", 0o750) + + # Copying to /etc/clamav to expose file as-is to administrator + self.copy_file("/var/lib/clamav/whitelist.ign2", "/etc/clamav/whitelist.ign2") diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index e60e7eef1..b7908502c 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -41,7 +41,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS -D ENABLE_MILTER=ON \ -D ENABLE_MAN_PAGES=OFF \ -D ENABLE_STATIC_LIB=OFF \ - -D ENABLE_JSON_SHARED=ON \ + -D ENABLE_JSON_SHARED=ON \ && cmake --build . \ && make DESTDIR="/clamav" -j$(($(nproc) - 1)) install \ && rm -r "/clamav/usr/lib/pkgconfig/" \ @@ -88,23 +88,34 @@ RUN apk upgrade --no-cache \ pcre2 \ zlib \ libgcc \ + py3-pip \ && addgroup -S "clamav" && \ adduser -D -G "clamav" -h "/var/lib/clamav" -s "/bin/false" -S "clamav" && \ install -d -m 755 -g "clamav" -o "clamav" "/var/log/clamav" && \ chown -R clamav:clamav /var/lib/clamav +RUN pip install --break-system-packages \ + mysql-connector-python \ + jinja2 \ + redis \ + dnspython + + COPY --from=builder "/clamav" "/" -# init -COPY clamd.sh /clamd.sh -RUN chmod +x /sbin/tini -# healthcheck -COPY healthcheck.sh /healthcheck.sh -COPY clamdcheck.sh /usr/local/bin -RUN chmod +x /healthcheck.sh -RUN chmod +x /usr/local/bin/clamdcheck.sh +COPY data/Dockerfiles/bootstrap /bootstrap +COPY data/Dockerfiles/clamd/docker-entrypoint.sh /docker-entrypoint.sh +COPY data/Dockerfiles/clamd/clamd.sh /clamd.sh +COPY data/Dockerfiles/clamd/healthcheck.sh /healthcheck.sh +COPY data/Dockerfiles/clamd/clamdcheck.sh /usr/local/bin HEALTHCHECK --start-period=6m CMD "/healthcheck.sh" -ENTRYPOINT [] +RUN chmod +x /docker-entrypoint.sh \ + /clamd.sh \ + /healthcheck.sh \ + /usr/local/bin/clamdcheck.sh \ + /sbin/tini + +ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["/sbin/tini", "-g", "--", "/clamd.sh"] \ No newline at end of file diff --git a/data/Dockerfiles/clamd/clamd.sh b/data/Dockerfiles/clamd/clamd.sh index 2c6e75dc6..29e870e8a 100755 --- a/data/Dockerfiles/clamd/clamd.sh +++ b/data/Dockerfiles/clamd/clamd.sh @@ -1,48 +1,5 @@ #!/bin/bash -if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo "SKIP_CLAMD=y, skipping ClamAV..." - sleep 365d - exit 0 -fi - -# Cleaning up garbage -echo "Cleaning up tmp files..." -rm -rf /var/lib/clamav/clamav-*.tmp - -# Prepare whitelist - -mkdir -p /run/clamav /var/lib/clamav - -if [[ -s /etc/clamav/whitelist.ign2 ]]; then - echo "Copying non-empty whitelist.ign2 to /var/lib/clamav/whitelist.ign2" - cp /etc/clamav/whitelist.ign2 /var/lib/clamav/whitelist.ign2 -fi - -if [[ ! -f /var/lib/clamav/whitelist.ign2 ]]; then - echo "Creating /var/lib/clamav/whitelist.ign2" - cat < /var/lib/clamav/whitelist.ign2 -# Please restart ClamAV after changing signatures -Example-Signature.Ignore-1 -PUA.Win.Trojan.EmbeddedPDF-1 -PUA.Pdf.Trojan.EmbeddedJavaScript-1 -PUA.Pdf.Trojan.OpenActionObjectwithJavascript-1 -EOF -fi - -chown clamav:clamav -R /var/lib/clamav /run/clamav - -chmod 755 /var/lib/clamav -chmod 644 -R /var/lib/clamav/* -chmod 750 /run/clamav - -stat /var/lib/clamav/whitelist.ign2 -dos2unix /var/lib/clamav/whitelist.ign2 -sed -i '/^\s*$/d' /var/lib/clamav/whitelist.ign2 -# Copying to /etc/clamav to expose file as-is to administrator -cp -p /var/lib/clamav/whitelist.ign2 /etc/clamav/whitelist.ign2 - - BACKGROUND_TASKS=() echo "Running freshclam..." diff --git a/data/Dockerfiles/clamd/docker-entrypoint.sh b/data/Dockerfiles/clamd/docker-entrypoint.sh new file mode 100644 index 000000000..59b44343b --- /dev/null +++ b/data/Dockerfiles/clamd/docker-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Run hooks +for file in /hooks/*; do + if [ -x "${file}" ]; then + echo "Running hook ${file}" + "${file}" + fi +done + +python3 -u /bootstrap/main.py +BOOTSTRAP_EXIT_CODE=$? + +if [ $BOOTSTRAP_EXIT_CODE -ne 0 ]; then + echo "Bootstrap failed with exit code $BOOTSTRAP_EXIT_CODE. Not starting Clamd." + exit $BOOTSTRAP_EXIT_CODE +fi + +echo "Bootstrap succeeded. Starting Clamd..." +exec "$@" diff --git a/data/conf/clamav/config_templates/whitelist.ign2.j2 b/data/conf/clamav/config_templates/whitelist.ign2.j2 new file mode 100644 index 000000000..5a9f5ae5b --- /dev/null +++ b/data/conf/clamav/config_templates/whitelist.ign2.j2 @@ -0,0 +1,5 @@ +# Please restart ClamAV after changing signatures +Example-Signature.Ignore-1 +PUA.Win.Trojan.EmbeddedPDF-1 +PUA.Pdf.Trojan.EmbeddedJavaScript-1 +PUA.Pdf.Trojan.OpenActionObjectwithJavascript-1 diff --git a/data/hooks/clamd/.gitkeep b/data/hooks/clamd/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docker-compose.yml b/docker-compose.yml index b315c6e35..e8cb8e653 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,7 +65,7 @@ services: - redis clamd-mailcow: - image: ghcr.io/mailcow/clamd:1.70 + image: ghcr.io/mailcow/clamd:nightly-19052025 restart: always depends_on: unbound-mailcow: @@ -73,10 +73,15 @@ services: dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: + - CONTAINER_NAME=clamd-mailcow + - DBNAME=${DBNAME} + - DBUSER=${DBUSER} + - DBPASS=${DBPASS} - TZ=${TZ} - SKIP_CLAMD=${SKIP_CLAMD:-n} volumes: - ./data/conf/clamav/:/etc/clamav/:Z + - mysql-socket-vol-1:/var/run/mysqld/ - clamd-db-vol-1:/var/lib/clamav networks: mailcow-network: