feat(deploy): 支持配置邮件客户端 TLS 证书
- 新增 `LANQIN_TLS_CERT_FILE` 和 `LANQIN_TLS_KEY_FILE`,让 Postfix 与 Dovecot 使用外部证书。 - 在单容器与分服务启动脚本中统一回退到 `localhost` 自签证书,并在证书不可读时给出告警。 - 补充部署文档与示例配置,说明第三方客户端校验证书的挂载方式。
This commit is contained in:
@@ -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=
|
||||||
|
|
||||||
# =========================
|
# =========================
|
||||||
# 初始管理员
|
# 初始管理员
|
||||||
# =========================
|
# =========================
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 &
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user