feat(deploy): 支持配置邮件客户端 TLS 证书

- 新增 `LANQIN_TLS_CERT_FILE` 和 `LANQIN_TLS_KEY_FILE`,让 Postfix 与 Dovecot 使用外部证书。
- 在单容器与分服务启动脚本中统一回退到 `localhost` 自签证书,并在证书不可读时给出告警。
- 补充部署文档与示例配置,说明第三方客户端校验证书的挂载方式。
This commit is contained in:
LanQin
2026-06-17 00:05:23 +08:00
parent 27ab80adb9
commit 811267e5b6
7 changed files with 93 additions and 0 deletions
+6
View File
@@ -26,6 +26,12 @@ LANQIN_PUBLIC_HOSTNAME=mail.example.com
# Webmail 对外访问地址。生产环境建议使用 https。 # Webmail 对外访问地址。生产环境建议使用 https。
LANQIN_PUBLIC_BASE_URL=https://mail.example.com LANQIN_PUBLIC_BASE_URL=https://mail.example.com
# 邮件客户端 TLS 证书。用于 SMTP 465/587、IMAP 993、POP3 995。
# 生产环境请挂载真实证书,并确保域名包含 LANQIN_PUBLIC_HOSTNAME。
# 留空时会使用容器自带 localhost 自签证书,第三方客户端会提示证书不匹配。
LANQIN_TLS_CERT_FILE=
LANQIN_TLS_KEY_FILE=
# ========================= # =========================
# 初始管理员 # 初始管理员
# ========================= # =========================
+30
View File
@@ -129,6 +129,36 @@ docker compose -f docker-compose.stack.yml -f docker-compose.stack.build.yml up
- Go API 是 Webmail 和管理后台入口;浏览器不直接连接 SMTP/IMAP/POP3。 - Go API 是 Webmail 和管理后台入口;浏览器不直接连接 SMTP/IMAP/POP3。
- Go API 会读取 `LANQIN_MAILDIR_ROOT=/var/mail/vhosts`,周期扫描 Maildir,把 Postfix/Dovecot 入站邮件同步成 Webmail 索引。 - Go API 会读取 `LANQIN_MAILDIR_ROOT=/var/mail/vhosts`,周期扫描 Maildir,把 Postfix/Dovecot 入站邮件同步成 Webmail 索引。
## 邮件客户端 TLS 证书
Web 站点可以由宿主机 Nginx / 宝塔反代到容器 `80`,但 SMTP/IMAP/POP3 端口不会使用 Web 反代的证书。
如果第三方客户端连接 `465/587/993/995` 时提示证书是 `localhost`,说明 Postfix/Dovecot 仍在使用容器自带的测试证书。
生产环境请把域名证书挂载进容器,并在 `.env` 指向证书文件:
```env
LANQIN_TLS_CERT_FILE=/certs/fullchain.pem
LANQIN_TLS_KEY_FILE=/certs/privkey.pem
```
单容器示例:
```yaml
services:
lanqin-email:
volumes:
- ./data:/data
- ./mail:/var/mail/vhosts
- ./dkim:/var/lib/rspamd/dkim
- /etc/letsencrypt:/etc/letsencrypt:ro
```
证书域名必须覆盖 `LANQIN_PUBLIC_HOSTNAME`。更新后执行:
```bash
docker compose up -d --force-recreate
```
## SMTP 发信排查 ## SMTP 发信排查
单容器部署时,Webmail 发信默认提交给同容器内的 Postfix: 单容器部署时,Webmail 发信默认提交给同容器内的 Postfix:
+17
View File
@@ -8,6 +8,8 @@ set -eu
: "${LANQIN_SMTP_HOST:=127.0.0.1}" : "${LANQIN_SMTP_HOST:=127.0.0.1}"
: "${LANQIN_SMTP_PORT:=25}" : "${LANQIN_SMTP_PORT:=25}"
: "${LANQIN_MAILDIR_ROOT:=/var/mail/vhosts}" : "${LANQIN_MAILDIR_ROOT:=/var/mail/vhosts}"
: "${LANQIN_TLS_CERT_FILE:=}"
: "${LANQIN_TLS_KEY_FILE:=}"
export LANQIN_DATA_DIR LANQIN_DB_PATH LANQIN_ADDR LANQIN_SMTP_HOST LANQIN_SMTP_PORT LANQIN_MAILDIR_ROOT export LANQIN_DATA_DIR LANQIN_DB_PATH LANQIN_ADDR LANQIN_SMTP_HOST LANQIN_SMTP_PORT LANQIN_MAILDIR_ROOT
@@ -21,13 +23,28 @@ elif id rspamd >/dev/null 2>&1; then
chown -R rspamd:rspamd /run/rspamd /var/lib/rspamd 2>/dev/null || true chown -R rspamd:rspamd /run/rspamd /var/lib/rspamd 2>/dev/null || true
fi fi
TLS_CERT=/etc/ssl/certs/ssl-cert-snakeoil.pem
TLS_KEY=/etc/ssl/private/ssl-cert-snakeoil.key
if [ -n "$LANQIN_TLS_CERT_FILE" ] || [ -n "$LANQIN_TLS_KEY_FILE" ]; then
if [ -f "$LANQIN_TLS_CERT_FILE" ] && [ -f "$LANQIN_TLS_KEY_FILE" ]; then
TLS_CERT="$LANQIN_TLS_CERT_FILE"
TLS_KEY="$LANQIN_TLS_KEY_FILE"
else
echo "warning: LANQIN_TLS_CERT_FILE/LANQIN_TLS_KEY_FILE not readable; using snakeoil localhost certificate" >&2
fi
fi
postconf -e "myhostname = ${LANQIN_PUBLIC_HOSTNAME}" postconf -e "myhostname = ${LANQIN_PUBLIC_HOSTNAME}"
postconf -e "myorigin = ${LANQIN_PUBLIC_HOSTNAME}" postconf -e "myorigin = ${LANQIN_PUBLIC_HOSTNAME}"
postconf -e "smtpd_tls_cert_file = ${TLS_CERT}"
postconf -e "smtpd_tls_key_file = ${TLS_KEY}"
postconf -e "virtual_transport = lmtp:inet:127.0.0.1:24" 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_sasl_path = inet:127.0.0.1:12345"
postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}" 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 "smtpd_milters = inet:127.0.0.1:11332"
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332" postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
sed -i "s#^ssl_cert = <.*#ssl_cert = <${TLS_CERT}#" /etc/dovecot/dovecot.conf
sed -i "s#^ssl_key = <.*#ssl_key = <${TLS_KEY}#" /etc/dovecot/dovecot.conf
# Rspamd DKIM keys are exported 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 & /usr/local/bin/lanqin-api >/tmp/lanqin-api-bootstrap.log 2>&1 &
+8
View File
@@ -32,6 +32,10 @@ services:
volumes: volumes:
- ./data:/data - ./data:/data
- ./mail:/var/mail/vhosts - ./mail:/var/mail/vhosts
# 生产环境如需第三方客户端校验证书,请取消下面挂载,并在 .env 配置:
# LANQIN_TLS_CERT_FILE=/etc/letsencrypt/live/mail.example.com/fullchain.pem
# LANQIN_TLS_KEY_FILE=/etc/letsencrypt/live/mail.example.com/privkey.pem
# - /etc/letsencrypt:/etc/letsencrypt:ro
ports: ports:
- "25:25" - "25:25"
- "465:465" - "465:465"
@@ -47,6 +51,10 @@ services:
volumes: volumes:
- ./data:/data - ./data:/data
- ./mail:/var/mail/vhosts - ./mail:/var/mail/vhosts
# 生产环境如需第三方客户端校验证书,请取消下面挂载,并在 .env 配置:
# LANQIN_TLS_CERT_FILE=/etc/letsencrypt/live/mail.example.com/fullchain.pem
# LANQIN_TLS_KEY_FILE=/etc/letsencrypt/live/mail.example.com/privkey.pem
# - /etc/letsencrypt:/etc/letsencrypt:ro
ports: ports:
- "993:993" - "993:993"
- "995:995" - "995:995"
+4
View File
@@ -14,4 +14,8 @@ services:
- ./data:/data - ./data:/data
- ./mail:/var/mail/vhosts - ./mail:/var/mail/vhosts
- ./dkim:/var/lib/rspamd/dkim - ./dkim:/var/lib/rspamd/dkim
# 生产环境如需第三方客户端校验证书,请取消下面挂载,并在 .env 配置:
# LANQIN_TLS_CERT_FILE=/etc/letsencrypt/live/mail.example.com/fullchain.pem
# LANQIN_TLS_KEY_FILE=/etc/letsencrypt/live/mail.example.com/privkey.pem
# - /etc/letsencrypt:/etc/letsencrypt:ro
restart: unless-stopped restart: unless-stopped
+14
View File
@@ -1,7 +1,21 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
: "${LANQIN_TLS_CERT_FILE:=}"
: "${LANQIN_TLS_KEY_FILE:=}"
addgroup --system --gid 5000 vmail 2>/dev/null || true 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 adduser --system --uid 5000 --gid 5000 --home /var/mail/vhosts --no-create-home vmail 2>/dev/null || true
mkdir -p /var/mail/vhosts mkdir -p /var/mail/vhosts
chown -R 5000:5000 /var/mail/vhosts chown -R 5000:5000 /var/mail/vhosts
TLS_CERT=/etc/ssl/certs/ssl-cert-snakeoil.pem
TLS_KEY=/etc/ssl/private/ssl-cert-snakeoil.key
if [ -n "$LANQIN_TLS_CERT_FILE" ] || [ -n "$LANQIN_TLS_KEY_FILE" ]; then
if [ -f "$LANQIN_TLS_CERT_FILE" ] && [ -f "$LANQIN_TLS_KEY_FILE" ]; then
TLS_CERT="$LANQIN_TLS_CERT_FILE"
TLS_KEY="$LANQIN_TLS_KEY_FILE"
else
echo "warning: LANQIN_TLS_CERT_FILE/LANQIN_TLS_KEY_FILE not readable; using snakeoil localhost certificate" >&2
fi
fi
sed -i "s#^ssl_cert = <.*#ssl_cert = <${TLS_CERT}#" /etc/dovecot/dovecot.conf
sed -i "s#^ssl_key = <.*#ssl_key = <${TLS_KEY}#" /etc/dovecot/dovecot.conf
exec dovecot -F exec dovecot -F
+14
View File
@@ -1,8 +1,22 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
: "${LANQIN_PUBLIC_HOSTNAME:=mail.example.com}" : "${LANQIN_PUBLIC_HOSTNAME:=mail.example.com}"
: "${LANQIN_TLS_CERT_FILE:=}"
: "${LANQIN_TLS_KEY_FILE:=}"
TLS_CERT=/etc/ssl/certs/ssl-cert-snakeoil.pem
TLS_KEY=/etc/ssl/private/ssl-cert-snakeoil.key
if [ -n "$LANQIN_TLS_CERT_FILE" ] || [ -n "$LANQIN_TLS_KEY_FILE" ]; then
if [ -f "$LANQIN_TLS_CERT_FILE" ] && [ -f "$LANQIN_TLS_KEY_FILE" ]; then
TLS_CERT="$LANQIN_TLS_CERT_FILE"
TLS_KEY="$LANQIN_TLS_KEY_FILE"
else
echo "warning: LANQIN_TLS_CERT_FILE/LANQIN_TLS_KEY_FILE not readable; using snakeoil localhost certificate" >&2
fi
fi
postconf -e "myhostname = ${LANQIN_PUBLIC_HOSTNAME}" postconf -e "myhostname = ${LANQIN_PUBLIC_HOSTNAME}"
postconf -e "myorigin = ${LANQIN_PUBLIC_HOSTNAME}" postconf -e "myorigin = ${LANQIN_PUBLIC_HOSTNAME}"
postconf -e "smtpd_tls_cert_file = ${TLS_CERT}"
postconf -e "smtpd_tls_key_file = ${TLS_KEY}"
postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}" postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"
postconf -e "smtpd_milters = inet:rspamd:11332" postconf -e "smtpd_milters = inet:rspamd:11332"
postconf -e "non_smtpd_milters = inet:rspamd:11332" postconf -e "non_smtpd_milters = inet:rspamd:11332"