diff --git a/deploy/.env.example b/deploy/.env.example index 099f774..174bdd6 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -26,6 +26,12 @@ LANQIN_PUBLIC_HOSTNAME=mail.example.com # Webmail 对外访问地址。生产环境建议使用 https。 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= + # ========================= # 初始管理员 # ========================= diff --git a/deploy/README.md b/deploy/README.md index accc687..a9274d2 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -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 会读取 `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 发信排查 单容器部署时,Webmail 发信默认提交给同容器内的 Postfix: diff --git a/deploy/all-in-one/entrypoint.sh b/deploy/all-in-one/entrypoint.sh index c5a0123..569f9d6 100644 --- a/deploy/all-in-one/entrypoint.sh +++ b/deploy/all-in-one/entrypoint.sh @@ -8,6 +8,8 @@ set -eu : "${LANQIN_SMTP_HOST:=127.0.0.1}" : "${LANQIN_SMTP_PORT:=25}" : "${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 @@ -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 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 "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 "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 "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. /usr/local/bin/lanqin-api >/tmp/lanqin-api-bootstrap.log 2>&1 & diff --git a/deploy/docker-compose.stack.yml b/deploy/docker-compose.stack.yml index c3a6987..d3bd00c 100644 --- a/deploy/docker-compose.stack.yml +++ b/deploy/docker-compose.stack.yml @@ -32,6 +32,10 @@ services: volumes: - ./data:/data - ./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: - "25:25" - "465:465" @@ -47,6 +51,10 @@ services: volumes: - ./data:/data - ./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: - "993:993" - "995:995" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 329cec9..ab4de3f 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -14,4 +14,8 @@ services: - ./data:/data - ./mail:/var/mail/vhosts - ./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 diff --git a/deploy/dovecot/entrypoint.sh b/deploy/dovecot/entrypoint.sh index 893c2f2..dbe2ae4 100644 --- a/deploy/dovecot/entrypoint.sh +++ b/deploy/dovecot/entrypoint.sh @@ -1,7 +1,21 @@ #!/bin/sh set -eu +: "${LANQIN_TLS_CERT_FILE:=}" +: "${LANQIN_TLS_KEY_FILE:=}" 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 /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 diff --git a/deploy/postfix/entrypoint.sh b/deploy/postfix/entrypoint.sh index e25005a..ef50303 100644 --- a/deploy/postfix/entrypoint.sh +++ b/deploy/postfix/entrypoint.sh @@ -1,8 +1,22 @@ #!/bin/sh set -eu : "${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 "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 "smtpd_milters = inet:rspamd:11332" postconf -e "non_smtpd_milters = inet:rspamd:11332"