# cpanel-importer — sanitization sandbox for cPanel cpmove tarballs. # # See cpanel-import-container-spec.md §1 for the full design. # # Build: docker build -t cpanel-importer:dev . # Run: see README.md for the docker run invocation the WHP panel uses. FROM almalinux:10-minimal LABEL org.opencontainers.image.title="cpanel-importer" LABEL org.opencontainers.image.description="cPanel cpmove sanitization sandbox (ClamAV + SaneSecurity + WP content scan)" LABEL org.opencontainers.image.source="https://repo.anhonesthost.net/cloud-hosting-platform/cpanel-importer" LABEL org.opencontainers.image.licenses="MIT" ARG TARGETARCH=amd64 # UID/GID of the unprivileged worker. Matches the spec — panel calls # `docker run --user 999:999`, so this UID must actually exist inside the # image (the EPEL `clamav` and `php` user accounts collide with low UIDs; # 999 is well clear of them). ARG WHP_UID=999 ARG WHP_GID=999 ENV LANG=C.UTF-8 \ LC_ALL=C.UTF-8 \ PHP_INI_DIR=/etc/php.d # Single RUN to minimize layers and image size. Cleans dnf cache and # the SaneSecurity rsync temp files at the end of the layer. # # Pinning strategy: # - PHP 8.4: AlmaLinux 10 stock ships PHP 8.3 only; the spec asks for # 8.4 specifically. We add Remi's modular repo and enable the # `php:remi-8.4` stream. We DO NOT pin to a specific 8.4.X because # Remi rolls security patches into the same minor and an exact pin # would block updates. # - clamav / clamav-update: track the AL10 EPEL stream. CI builds # monthly so signature DB age is bounded. # - SaneSecurity: rsync at build time, then again at container start # via `freshclam` (with the SaneSecurity third-party DBs configured). # # Ordering note: clamav-filesystem's RPM scripts auto-create a # `virusgroup` system group at the next free GID. If we let dnf install # clamav first, that lands at GID 999 — which then collides with the # UID/GID we want for whp-import. We pre-create our user FIRST so # virusgroup ends up at 998. RUN set -eux; \ # microdnf is what almalinux:10-minimal ships with by default. microdnf -y install --setopt=install_weak_deps=0 \ epel-release \ dnf \ shadow-utils \ ; \ # Add Remi's repo for PHP 8.4 (AL10 stock has 8.3 only). dnf -y --setopt=install_weak_deps=0 install \ https://rpms.remirepo.net/enterprise/remi-release-10.rpm ; \ dnf -y --setopt=install_weak_deps=0 module reset php ; \ dnf -y --setopt=install_weak_deps=0 module enable php:remi-8.4 ; \ # Pre-create the worker BEFORE installing clamav so virusgroup # doesn't claim our GID. groupadd --system --gid ${WHP_GID} whp-import ; \ useradd --system --uid ${WHP_UID} --gid ${WHP_GID} \ --home-dir /opt/whp --no-create-home \ --shell /sbin/nologin whp-import ; \ dnf -y --setopt=install_weak_deps=0 install \ php-cli \ php-json \ php-mbstring \ php-pdo \ php-mysqlnd \ php-xml \ php-zip \ php-process \ clamav \ clamav-update \ tar \ gzip \ bzip2 \ xz \ mariadb \ rsync \ ca-certificates \ coreutils-single \ findutils \ which \ ; \ mkdir -p /opt/whp /scripts /host/backup /host/quarantine /host/sanitized \ /var/lib/clamav /var/log/clamav ; \ # /opt/whp + /var/log/clamav owned by worker now. /var/lib/clamav # ownership is set AFTER the freshclam build-time pull below — root # has to be able to write there during the build. chown -R whp-import:whp-import /opt/whp /var/log/clamav ; \ # /host/quarantine and /host/sanitized are the bind-mount RW # targets. The panel chowns the HOST paths to UID 999 before # invocation (see README.md). When the host path is empty Docker # copies the IMAGE-side dir's ownership onto the new volume; we # need that ownership to be whp-import so an empty bind mount on # those paths still results in a writable volume. (Bind mounts to # an EXISTING host dir keep host ownership and are independent of # this — the panel sets up its own dirs with mode 750 owner 999.) chown whp-import:whp-import /host/quarantine /host/sanitized ; \ # Strip dnf cache. dnf -y clean all ; \ rm -rf /var/cache/dnf /var/cache/yum /var/cache/ldconfig/* \ /usr/share/doc /usr/share/man /usr/share/info # Pre-seed ClamAV signature databases at build time so the first # container run isn't dependent on freshclam succeeding before the scan. # # We do two passes: # 1. freshclam (mainline ClamAV signatures: main.cvd, daily.cvd, bytecode.cvd). # 2. rsync the SaneSecurity Foxhole.PHP DB — PHP-malware-focused, this # is the high-value addition for our use case. Junkemailfilter rules # are deliberately skipped (we don't scan email here). # # Both runs are wrapped in `|| true` so a transient network failure # during build does not break the image build; the container also runs # `freshclam` on start so a stale baseline gets refreshed at runtime. COPY configs/freshclam.conf /etc/freshclam.conf COPY configs/sanesecurity-mirror.txt /opt/whp/sanesecurity-mirror.txt # Pre-seed signatures as root, then chown the result. We don't ship the # privilege-switching tools (runuser/su are in util-linux full, ~2MB we # don't need at runtime) — the worker only needs to READ /var/lib/clamav # and the runtime freshclam refresh runs as the same UID 999 anyway, so # ownership matters there. RUN set -eux; \ chown whp-import:whp-import /etc/freshclam.conf ; \ # Mainline ClamAV DB pull at build time so we have something to scan # against even if the runtime freshclam refresh fails (e.g., no net). # freshclam has a compile-time default --user=clamupdate (UID 997) # and tries to setuid() to it; the build-time dir is whp-import-owned # so we tell it explicitly to stay as root for this one-shot pull. freshclam --no-warnings --user=root || \ echo "WARN: freshclam failed during build; runtime refresh will retry" ; \ # SaneSecurity Foxhole.PHP rules. The project rotates mirrors; the # file we COPYed lists the working rsync mirror used at build time. SANE_MIRROR="$(cat /opt/whp/sanesecurity-mirror.txt)" ; \ rsync -av --no-motd --contimeout=30 \ --include='foxhole_filename.cdb' \ --include='foxhole_filename.cdb.sig' \ --include='foxhole_generic.cdb' \ --include='foxhole_generic.cdb.sig' \ --include='foxhole_js.cdb' \ --include='foxhole_js.cdb.sig' \ --include='foxhole_js.ndb' \ --include='foxhole_js.ndb.sig' \ --include='foxhole_mail.cdb' \ --include='foxhole_mail.cdb.sig' \ --include='foxhole_all.ndb' \ --include='foxhole_all.ndb.sig' \ --exclude='*' \ "rsync://${SANE_MIRROR}/sanesecurity/" /var/lib/clamav/ \ || echo "WARN: SaneSecurity rsync failed during build; runtime freshclam will retry" ; \ chown -R whp-import:whp-import /var/lib/clamav ; \ chmod -R u=rwX,g=rX,o= /var/lib/clamav ; \ ls -la /var/lib/clamav/ COPY --chown=whp-import:whp-import scripts/ /scripts/ RUN chmod 0755 /scripts/entrypoint.sh /scripts/extract.sh \ /scripts/scan-files.php /scripts/scan-dbs.php WORKDIR /opt/whp USER whp-import # stdin is closed — the container reads its inputs from env + bind mounts. ENTRYPOINT ["/scripts/entrypoint.sh"]