chore(deploy): 将 DKIM 组件切换为 Rspamd
- 用 Rspamd 替换 OpenDKIM 及其相关镜像、编排和启动脚本。 - 调整 Postfix、单容器镜像与 Docker 配置,改为对接 Rspamd milter。 - 增加 DKIM 私钥同步脚本与 Rspamd 本地配置,支持从 SQLite 周期导出签名密钥。 - 更新 API 启动与测试逻辑,兼容新的默认管理员邮箱与邮箱加载方式。
This commit is contained in:
+4
-1
@@ -14,7 +14,7 @@ LANQIN_API_IMAGE=ghcr.io/lanqin996/lanqin-email-api:latest
|
||||
LANQIN_WEB_IMAGE=ghcr.io/lanqin996/lanqin-email-web:latest
|
||||
LANQIN_POSTFIX_IMAGE=ghcr.io/lanqin996/lanqin-email-postfix:latest
|
||||
LANQIN_DOVECOT_IMAGE=ghcr.io/lanqin996/lanqin-email-dovecot:latest
|
||||
LANQIN_OPENDKIM_IMAGE=ghcr.io/lanqin996/lanqin-email-opendkim:latest
|
||||
LANQIN_RSPAMD_IMAGE=ghcr.io/lanqin996/lanqin-email-rspamd:latest
|
||||
|
||||
# =========================
|
||||
# 对外访问地址
|
||||
@@ -96,6 +96,9 @@ LANQIN_MAILDIR_ROOT=/var/mail/vhosts
|
||||
# 扫描 Maildir 的间隔,单位:秒。
|
||||
LANQIN_MAILDIR_SCAN_SECONDS=30
|
||||
|
||||
# Rspamd 从 SQLite 导出 DKIM 私钥到签名目录的间隔,单位:秒。
|
||||
LANQIN_RSPAMD_DKIM_SYNC_SECONDS=60
|
||||
|
||||
# 无人收件开关。false:未注册邮箱默认拒收;true:未注册收件地址进入全部邮件。
|
||||
LANQIN_CATCH_ALL_ENABLED=false
|
||||
|
||||
|
||||
+6
-4
@@ -34,7 +34,7 @@ lanqin-email
|
||||
- Nginx
|
||||
- Postfix
|
||||
- Dovecot
|
||||
- OpenDKIM
|
||||
- Rspamd
|
||||
|
||||
常用命令:
|
||||
|
||||
@@ -60,7 +60,7 @@ ghcr.io/lanqin996/lanqin-email-api:latest
|
||||
ghcr.io/lanqin996/lanqin-email-web:latest
|
||||
ghcr.io/lanqin996/lanqin-email-postfix:latest
|
||||
ghcr.io/lanqin996/lanqin-email-dovecot:latest
|
||||
ghcr.io/lanqin996/lanqin-email-opendkim:latest
|
||||
ghcr.io/lanqin996/lanqin-email-rspamd:latest
|
||||
```
|
||||
|
||||
如果拉取时报:
|
||||
@@ -92,7 +92,7 @@ docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
|
||||
|
||||
## 可选:多容器调试部署
|
||||
|
||||
如果需要分别查看 Postfix / Dovecot / OpenDKIM 日志,可以使用 stack 编排。
|
||||
如果需要分别查看 Postfix / Dovecot / Rspamd 日志,可以使用 stack 编排。
|
||||
|
||||
拉取镜像版:
|
||||
|
||||
@@ -123,7 +123,8 @@ docker compose -f docker-compose.stack.yml -f docker-compose.stack.build.yml up
|
||||
|
||||
- Postfix 读取 `/data/lanqin.db` 中的 `domains`、`mailboxes`、`aliases`。
|
||||
- Dovecot 读取同一个 SQLite 数据库进行邮箱认证,并使用 `/var/mail/vhosts` 作为 Maildir 根目录。
|
||||
- OpenDKIM 启动时从 SQLite 导出域名 DKIM 私钥到容器内 `/etc/opendkim/keys`。
|
||||
- Rspamd 通过 milter 接入 Postfix,负责 DKIM 签名和垃圾邮件标记。
|
||||
- Rspamd 会周期性从 SQLite 导出域名 DKIM 私钥到容器内 `/var/lib/rspamd/dkim`。
|
||||
- Go API 是 Webmail 和管理后台唯一入口;浏览器不直接连接 SMTP/IMAP。
|
||||
- Go API 会读取 `LANQIN_MAILDIR_ROOT=/var/mail/vhosts`,周期扫描 Maildir,把 Postfix/Dovecot 入站邮件同步成 Webmail 索引。
|
||||
|
||||
@@ -132,3 +133,4 @@ docker compose -f docker-compose.stack.yml -f docker-compose.stack.build.yml up
|
||||
- 建议在服务器或边缘网关配置 HTTPS。
|
||||
- 云厂商通常默认封禁 25 端口,需要单独申请解封。
|
||||
- SQLite 适合 V1 单机部署;多节点部署前迁移到 PostgreSQL,并把 Postfix/Dovecot maps 改为 PostgreSQL。
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
ca-certificates tzdata sqlite3 supervisor nginx \
|
||||
postfix postfix-sqlite \
|
||||
dovecot-core dovecot-imapd dovecot-lmtpd dovecot-sqlite ssl-cert \
|
||||
opendkim opendkim-tools
|
||||
rspamd
|
||||
|
||||
COPY --from=api-build /out/lanqin-api /usr/local/bin/lanqin-api
|
||||
COPY --from=web-build /src/apps/web/dist /usr/share/nginx/html
|
||||
@@ -41,10 +41,10 @@ COPY deploy/postfix/master.cf /etc/postfix/master.cf
|
||||
COPY deploy/postfix/sqlite-*.cf /etc/postfix/
|
||||
COPY deploy/dovecot/dovecot.conf /etc/dovecot/dovecot.conf
|
||||
COPY deploy/dovecot/dovecot-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext
|
||||
COPY deploy/opendkim/opendkim.conf /etc/opendkim.conf
|
||||
COPY deploy/opendkim/TrustedHosts /etc/opendkim/TrustedHosts
|
||||
COPY deploy/rspamd/local.d/ /etc/rspamd/local.d/
|
||||
COPY deploy/rspamd/sync-dkim.sh /usr/local/bin/lanqin-rspamd-sync-dkim
|
||||
|
||||
RUN chmod +x /entrypoint.sh && mkdir -p /run/nginx /var/mail/vhosts /data /etc/opendkim/keys
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/lanqin-rspamd-sync-dkim && mkdir -p /run/nginx /run/rspamd /var/mail/vhosts /data /var/lib/rspamd/dkim
|
||||
|
||||
EXPOSE 80 443 25 587 993
|
||||
CMD ["/entrypoint.sh"]
|
||||
|
||||
@@ -13,17 +13,23 @@ export LANQIN_DATA_DIR LANQIN_DB_PATH LANQIN_ADDR LANQIN_SMTP_HOST LANQIN_SMTP_P
|
||||
|
||||
addgroup --system --gid 5000 vmail 2>/dev/null || true
|
||||
adduser --system --uid 5000 --gid 5000 --home /var/mail/vhosts --no-create-home vmail 2>/dev/null || true
|
||||
mkdir -p /data /var/mail/vhosts /etc/opendkim/keys /var/spool/postfix /var/run/dovecot
|
||||
mkdir -p /data /var/mail/vhosts /var/lib/rspamd/dkim /run/rspamd /var/spool/postfix /var/run/dovecot
|
||||
chown -R 5000:5000 /var/mail/vhosts
|
||||
if id _rspamd >/dev/null 2>&1; then
|
||||
chown -R _rspamd:_rspamd /run/rspamd /var/lib/rspamd 2>/dev/null || true
|
||||
elif id rspamd >/dev/null 2>&1; then
|
||||
chown -R rspamd:rspamd /run/rspamd /var/lib/rspamd 2>/dev/null || true
|
||||
fi
|
||||
|
||||
postconf -e "myhostname = ${LANQIN_PUBLIC_HOSTNAME}"
|
||||
postconf -e "myorigin = ${LANQIN_PUBLIC_HOSTNAME}"
|
||||
postconf -e "virtual_transport = lmtp:inet:127.0.0.1:24"
|
||||
postconf -e "smtpd_sasl_path = inet:127.0.0.1:12345"
|
||||
postconf -e "smtpd_milters = inet:127.0.0.1:8891"
|
||||
postconf -e "non_smtpd_milters = inet:127.0.0.1:8891"
|
||||
postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"
|
||||
postconf -e "smtpd_milters = inet:127.0.0.1:11332"
|
||||
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
|
||||
|
||||
# OpenDKIM keys are generated after API seed/migrations create the SQLite DB.
|
||||
# Rspamd DKIM keys are exported after API seed/migrations create the SQLite DB.
|
||||
/usr/local/bin/lanqin-api >/tmp/lanqin-api-bootstrap.log 2>&1 &
|
||||
bootstrap_pid=$!
|
||||
for i in $(seq 1 60); do
|
||||
@@ -39,21 +45,7 @@ done
|
||||
kill "$bootstrap_pid" 2>/dev/null || true
|
||||
wait "$bootstrap_pid" 2>/dev/null || true
|
||||
|
||||
: > /etc/opendkim/KeyTable
|
||||
: > /etc/opendkim/SigningTable
|
||||
if [ -f "$LANQIN_DB_PATH" ]; then
|
||||
sqlite3 -separator '|' "$LANQIN_DB_PATH" "SELECT name, dkim_selector, dkim_private_key FROM domains WHERE status='active';" | while IFS='|' read -r domain selector private_key; do
|
||||
[ -n "$domain" ] || continue
|
||||
dir="/etc/opendkim/keys/$domain"
|
||||
mkdir -p "$dir"
|
||||
keyfile="$dir/$selector.private"
|
||||
printf '%s' "$private_key" | base64 -d > "$keyfile"
|
||||
chmod 600 "$keyfile"
|
||||
echo "$selector._domainkey.$domain $domain:$selector:$keyfile" >> /etc/opendkim/KeyTable
|
||||
echo "*@${domain} ${selector}._domainkey.${domain}" >> /etc/opendkim/SigningTable
|
||||
done
|
||||
fi
|
||||
chown -R opendkim:opendkim /etc/opendkim
|
||||
/usr/local/bin/lanqin-rspamd-sync-dkim --once || true
|
||||
|
||||
postfix check
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/lanqin.conf
|
||||
|
||||
@@ -24,8 +24,8 @@ stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=20
|
||||
|
||||
[program:opendkim]
|
||||
command=/usr/sbin/opendkim -f -x /etc/opendkim.conf
|
||||
[program:rspamd-dkim-sync]
|
||||
command=/usr/local/bin/lanqin-rspamd-sync-dkim
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
@@ -34,6 +34,16 @@ stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=30
|
||||
|
||||
[program:rspamd]
|
||||
command=/usr/bin/rspamd -f
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/fd/2
|
||||
stderr_logfile_maxbytes=0
|
||||
priority=35
|
||||
|
||||
[program:postfix]
|
||||
command=/usr/sbin/postfix start-fg
|
||||
autostart=true
|
||||
|
||||
@@ -17,6 +17,6 @@ services:
|
||||
build:
|
||||
context: ./dovecot
|
||||
|
||||
opendkim:
|
||||
rspamd:
|
||||
build:
|
||||
context: ./opendkim
|
||||
context: ./rspamd
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
- "587:587"
|
||||
depends_on:
|
||||
- dovecot
|
||||
- opendkim
|
||||
- rspamd
|
||||
restart: unless-stopped
|
||||
|
||||
dovecot:
|
||||
@@ -50,15 +50,15 @@ services:
|
||||
- "993:993"
|
||||
restart: unless-stopped
|
||||
|
||||
opendkim:
|
||||
image: ${LANQIN_OPENDKIM_IMAGE:-ghcr.io/lanqin996/lanqin-email-opendkim:latest}
|
||||
rspamd:
|
||||
image: ${LANQIN_RSPAMD_IMAGE:-ghcr.io/lanqin996/lanqin-email-rspamd:latest}
|
||||
env_file: .env
|
||||
volumes:
|
||||
- lanqin-data:/data:ro
|
||||
- dkim-keys:/etc/opendkim/keys
|
||||
- rspamd-dkim:/var/lib/rspamd/dkim
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
lanqin-data:
|
||||
maildata:
|
||||
dkim-keys:
|
||||
rspamd-dkim:
|
||||
|
||||
@@ -11,10 +11,10 @@ services:
|
||||
volumes:
|
||||
- lanqin-data:/data
|
||||
- maildata:/var/mail/vhosts
|
||||
- dkim-keys:/etc/opendkim/keys
|
||||
- rspamd-dkim:/var/lib/rspamd/dkim
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
lanqin-data:
|
||||
maildata:
|
||||
dkim-keys:
|
||||
rspamd-dkim:
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
mkdir -p /etc/opendkim/keys
|
||||
: > /etc/opendkim/KeyTable
|
||||
: > /etc/opendkim/SigningTable
|
||||
if [ -f /data/lanqin.db ]; then
|
||||
sqlite3 -separator '|' /data/lanqin.db "SELECT name, dkim_selector, dkim_private_key FROM domains WHERE status='active';" | while IFS='|' read -r domain selector private_key; do
|
||||
[ -n "$domain" ] || continue
|
||||
dir="/etc/opendkim/keys/$domain"
|
||||
mkdir -p "$dir"
|
||||
keyfile="$dir/$selector.private"
|
||||
printf '%s' "$private_key" | base64 -d > "$keyfile"
|
||||
chmod 600 "$keyfile"
|
||||
echo "$selector._domainkey.$domain $domain:$selector:$keyfile" >> /etc/opendkim/KeyTable
|
||||
echo "*@${domain} ${selector}._domainkey.${domain}" >> /etc/opendkim/SigningTable
|
||||
done
|
||||
fi
|
||||
chown -R opendkim:opendkim /etc/opendkim
|
||||
exec opendkim -f -x /etc/opendkim.conf
|
||||
@@ -1,12 +0,0 @@
|
||||
Syslog yes
|
||||
UMask 002
|
||||
Mode sv
|
||||
Canonicalization relaxed/simple
|
||||
SubDomains no
|
||||
OversignHeaders From
|
||||
Socket inet:8891@0.0.0.0
|
||||
UserID opendkim:opendkim
|
||||
KeyTable /etc/opendkim/KeyTable
|
||||
SigningTable refile:/etc/opendkim/SigningTable
|
||||
ExternalIgnoreList /etc/opendkim/TrustedHosts
|
||||
InternalHosts /etc/opendkim/TrustedHosts
|
||||
@@ -3,7 +3,8 @@ set -eu
|
||||
: "${LANQIN_PUBLIC_HOSTNAME:=mail.example.com}"
|
||||
postconf -e "myhostname = ${LANQIN_PUBLIC_HOSTNAME}"
|
||||
postconf -e "myorigin = ${LANQIN_PUBLIC_HOSTNAME}"
|
||||
postconf -e "smtpd_milters = inet:opendkim:8891"
|
||||
postconf -e "non_smtpd_milters = inet:opendkim:8891"
|
||||
postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"
|
||||
postconf -e "smtpd_milters = inet:rspamd:11332"
|
||||
postconf -e "non_smtpd_milters = inet:rspamd:11332"
|
||||
postfix check
|
||||
exec postfix start-fg
|
||||
|
||||
@@ -23,8 +23,9 @@ smtpd_sasl_auth_enable = yes
|
||||
smtpd_tls_security_level = may
|
||||
smtp_tls_security_level = may
|
||||
|
||||
# DKIM milter.
|
||||
# Rspamd milter: DKIM signing + spam headers.
|
||||
milter_protocol = 6
|
||||
milter_default_action = accept
|
||||
smtpd_milters = inet:opendkim:8891
|
||||
non_smtpd_milters = inet:opendkim:8891
|
||||
milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
|
||||
smtpd_milters = inet:rspamd:11332
|
||||
non_smtpd_milters = inet:rspamd:11332
|
||||
|
||||
@@ -5,10 +5,10 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean && \
|
||||
apt-get update && apt-get install -y --no-install-recommends opendkim opendkim-tools sqlite3 ca-certificates
|
||||
COPY opendkim.conf /etc/opendkim.conf
|
||||
COPY TrustedHosts /etc/opendkim/TrustedHosts
|
||||
apt-get update && apt-get install -y --no-install-recommends rspamd sqlite3 ca-certificates
|
||||
COPY local.d/ /etc/rspamd/local.d/
|
||||
COPY sync-dkim.sh /usr/local/bin/lanqin-rspamd-sync-dkim
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
EXPOSE 8891
|
||||
RUN chmod +x /entrypoint.sh /usr/local/bin/lanqin-rspamd-sync-dkim && mkdir -p /run/rspamd /var/lib/rspamd/dkim
|
||||
EXPOSE 11332 11334
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /run/rspamd /var/lib/rspamd/dkim
|
||||
if id _rspamd >/dev/null 2>&1; then
|
||||
chown -R _rspamd:_rspamd /run/rspamd /var/lib/rspamd 2>/dev/null || true
|
||||
elif id rspamd >/dev/null 2>&1; then
|
||||
chown -R rspamd:rspamd /run/rspamd /var/lib/rspamd 2>/dev/null || true
|
||||
fi
|
||||
|
||||
/usr/local/bin/lanqin-rspamd-sync-dkim --once || true
|
||||
/usr/local/bin/lanqin-rspamd-sync-dkim &
|
||||
exec rspamd -f
|
||||
@@ -0,0 +1,4 @@
|
||||
# V1 先把 Rspamd 作为 DKIM 签名和标记组件,不直接拒信,避免误杀。
|
||||
reject = 999;
|
||||
add_header = 6;
|
||||
greylist = 999;
|
||||
@@ -0,0 +1,19 @@
|
||||
enabled = true;
|
||||
|
||||
# LanQin API 固定生成 selector=lanqin。路径支持 $domain / $selector 模板。
|
||||
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
|
||||
selector = "lanqin";
|
||||
try_fallback = true;
|
||||
use_domain = "header";
|
||||
use_esld = false;
|
||||
use_redis = false;
|
||||
|
||||
# Go API / Postfix 在容器内提交的邮件需要签名。
|
||||
sign_authenticated = true;
|
||||
sign_local = true;
|
||||
sign_networks = "/etc/rspamd/local.d/sign_networks.map";
|
||||
|
||||
allow_username_mismatch = true;
|
||||
allow_hdrfrom_mismatch_local = true;
|
||||
allow_hdrfrom_mismatch_sign_networks = true;
|
||||
allow_hdrfrom_multiple = true;
|
||||
@@ -1,5 +1,4 @@
|
||||
127.0.0.1
|
||||
localhost
|
||||
127.0.0.0/8
|
||||
10.0.0.0/8
|
||||
172.16.0.0/12
|
||||
192.168.0.0/16
|
||||
@@ -0,0 +1,8 @@
|
||||
bind_socket = "0.0.0.0:11332";
|
||||
milter = yes;
|
||||
timeout = 120s;
|
||||
|
||||
upstream "local" {
|
||||
default = yes;
|
||||
self_scan = yes;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${LANQIN_DB_PATH:=/data/lanqin.db}"
|
||||
: "${LANQIN_RSPAMD_DKIM_DIR:=/var/lib/rspamd/dkim}"
|
||||
: "${LANQIN_RSPAMD_DKIM_SYNC_SECONDS:=60}"
|
||||
|
||||
chown_dkim_dir() {
|
||||
if id _rspamd >/dev/null 2>&1; then
|
||||
chown -R _rspamd:_rspamd "$LANQIN_RSPAMD_DKIM_DIR" 2>/dev/null || true
|
||||
elif id rspamd >/dev/null 2>&1; then
|
||||
chown -R rspamd:rspamd "$LANQIN_RSPAMD_DKIM_DIR" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
sync_keys() {
|
||||
mkdir -p "$LANQIN_RSPAMD_DKIM_DIR"
|
||||
if [ ! -f "$LANQIN_DB_PATH" ]; then
|
||||
chown_dkim_dir
|
||||
return 0
|
||||
fi
|
||||
|
||||
sqlite3 -separator '|' "$LANQIN_DB_PATH" "SELECT name, dkim_selector, dkim_private_key FROM domains WHERE status='active';" 2>/dev/null | while IFS='|' read -r domain selector private_key; do
|
||||
[ -n "$domain" ] || continue
|
||||
[ -n "$selector" ] || selector="lanqin"
|
||||
keyfile="$LANQIN_RSPAMD_DKIM_DIR/${domain}.${selector}.key"
|
||||
tmpfile="${keyfile}.tmp"
|
||||
printf '%s' "$private_key" | base64 -d > "$tmpfile"
|
||||
chmod 0640 "$tmpfile"
|
||||
mv "$tmpfile" "$keyfile"
|
||||
done
|
||||
|
||||
chown_dkim_dir
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "--once" ]; then
|
||||
sync_keys
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while true; do
|
||||
sync_keys || true
|
||||
sleep "$LANQIN_RSPAMD_DKIM_SYNC_SECONDS"
|
||||
done
|
||||
Reference in New Issue
Block a user