为自托管站点配置带 Mail-Receiver 的直接投递入站邮件

如何完全禁用 DMARC 支持?

也就是说,在 mail-receiver.ymlenv 部分添加 INCLUDE_DMARC: false 似乎并不能实现。这确实会导致 opendkimopendmarc 守护进程不运行(导致日志中出现警告),但 SPF 检查仍在进行。

编辑补充:
我认为通过在 env 部分添加以下 POSTCONF_ 行,我设法禁用了 SPF 检查:

env:
  ...
  INCLUDE_DMARC: false
  POSTCONF_smtpd_recipient_restrictions: check_policy_service unix:private/policy
  ...

我通过查看引入 DMARC 检查的 提交,并查看当 INCLUDE_DMARC 为 false 时应该发生什么,得到了这个。

我对 Docker 镜像的构建方式知之甚少,但我感觉 INCLUDE_DMARC 标志是为其他人、在其他地方、在其他时间设置的,而不是可以在 mail-receiver.yml 中完成的。

2 个赞

我在 ufw 中发现需要打开 443 端口 — 否则,我在 logs 中会遇到 API Request Preparation Failed。我认为最好提及这一点,因为标准的安装说明提到了启用 ufw。

mail-receiver.yml 中提到了 25 端口,它似乎绕过了 ufw。

1 个赞

GitHub 仓库是否应放在 OP 中?

3 个赞

mail-receiver 的用户,请参阅 Remove smtp_should_reject & discourse-smtp-fast-rejection

我们将完全移除快速拒绝(fast-rejection)功能,因为最初的功能存在缺陷并给用户带来了问题,特别是以下情况:

它还会影响转发邮件,因为预投递测试检查的是 envelope-from 和 envelope-to,而 Discourse 只使用邮件头中的值。

1 个赞

我刚刚提交了这个 PR,用于删除 mail-receiver.yml 示例文件中 DISCOURSE_BASE_URL 值周围不必要的引号。这些引号破坏了我的设置。去掉这些引号可以使本文档成功完成。

你能解释一下如何破坏的吗?这个值周围有无引号没有产生任何区别:

[2] pry(main)=> YAML::load("env:\n  DISCOURSE_BASE_URL: 'https://discourse.example.com'")
=> {"env"=>{"DISCOURSE_BASE_URL"=>"https://discourse.example.com"}}

[3] pry(main)=> YAML::load("env:\n  DISCOURSE_BASE_URL: https://discourse.example.com")
=> {"env"=>{"DISCOURSE_BASE_URL"=>"https://discourse.example.com"}}

当我追踪该容器的日志并向其发送消息时,我看到一堆错误,提到类似“discourse.example.com 不是 MX 记录的一部分”之类的信息。我删除了引号,重建了容器,它就开始工作了 :person_shrugging:

事件的顺序可能也很重要:

  1. 我配置并启动了邮件接收器(mail-receiver)容器
  2. 几天后我设置好了 MX DNS 记录
  3. 我验证了 MX 记录设置正确,然后开始测试。它没有工作
  4. 删除了引号,重建了容器,开始工作了

所以我不确定是引号的移除导致了解析,还是在创建 MX 记录后重建容器导致了解析。

最坏的情况是,这个拉取请求(PR)让 yml 文件看起来更一致 :slight_smile:

1 个赞

它似乎假设邮件接收方将始终与基础论坛相同的域名。在不满足此条件的情况下,我们如何设置 TLS?

例如:
forum => forum.domain.tld
mail-receiver => mail.domain.tld

mail-receiver.yml 中,TLS 指向基础论坛证书。是否有办法让邮件接收方获取其自身的证书?

我不知道直接的答案,但我怀疑这需要在 yml 中添加额外的选项,以便在构建期间在容器中进行修改。

更多信息稍后提供,但我好奇您想在不同域上运行它的原因是什么。邮件接收器是专门为此定制的,并且在不修改的情况下,只能接收与其配对的 Discourse 实例的电子邮件,因此通常最好让它与该实例在同一域上运行。


如果您查看一些用于包含在您的 Discourse yml 中的模板(其中一些可能已经在使用),您应该能够获得如何通过 yml(在容器构建期间)运行命令和修改文件的提示。

web.onion.template.yml 包含一些关于如何替换文件中字符串的示例,而 web.letsencrypt.ssl.template.yml 是向主 Discourse 容器添加 Let’s Encrypt 的那个。

我不知道其中有多少依赖于基础镜像中的内容,所以可能更简单的方法是让主 Discourse 容器获取第二个证书,然后只需将 mail-receiver.yml 中的证书/密钥路径更改为匹配即可。

请注意,如果您采用这种方法进行此类更改,请务必确切了解更改将产生什么影响。Let’s Encrypt 相关的错误更改可能会导致证书续订静默失败,例如,您可能要等到大约 3 个月后访问者开始收到过期证书错误时才会注意到。

CloudFlare 用例

这些说明适用于使用 Cloudflare 代理的自托管 Discourse 论坛。

当您使用 Cloudflare 代理时,这会阻止所有 SMTP(端口 25)流量到达您的服务器。这要求您为邮件接收器(mail-receiver)设置一个不同的子域名才能正常工作。

例如,如果您的域名是 forums.domain.tld,则需要创建一个新的子域名,例如 mail.domain.tld

使用 Cloudflare 时,您需要执行以下附加步骤。

  1. 为新子域名创建 A 记录。它将使用与您的 forums.domain.tld 相同的 IP 地址。
  2. 按照主说明中提供的方式为新子域名创建 MX 记录。

遵循主要说明,只需进行此微小更改。在关闭 TLS 安全的情况下,它可以正常工作。

如果您想运行 TLS 安全,则需要进行额外的工作。

TLS 设置概述

这些说明将安装 Certbot 和一个 CloudFlare Certbot 插件。这些命令将通过 DNS 认证过程以独立模式(Standalone)获取 Let’s Encrypt 证书。一旦证书可用,它们将被复制到邮件接收器的共享区域供容器使用。我们必须使用 DNS 模型,因为 Discourse 已经占用了端口 80。

DNS 挑战

Certbot 不再通过 HTTP 证明域名所有权,而是通过在您的 DNS 中创建 TXT 记录来证明。由于您的 DNS 是 Cloudflare,这可以通过 Cloudflare API 令牌完全自动化——无需端口 80,无需关闭 Web 服务器。

工作原理

Certbot → 在 Cloudflare 中创建 _acme-challenge.mail.lotuselan.net TXT 记录
Let's Encrypt → 查找该 TXT 记录 → 验证 → 发布证书
Certbot → 删除 TXT 记录

所有这些都在您的基础服务器上完成,而不是在 discourse 容器内完成。

设置

1 — 安装 cerbot 和 Cloudflare certbot 插件:

bash

apt install certbot python3-certbot-dns-cloudflare -y

2 — 创建一个 Cloudflare API 令牌:

  1. 转到 Cloudflare → 我的个人资料 → API 令牌 → 创建令牌
  2. 使用 “编辑区域 DNS” 模板
  3. 权限:区域 → DNS → 编辑
  4. 区域资源:包括 → 特定区域 → lotuselan.net
  5. IP 限制:仅设置为允许来自您的服务器 IP 地址的访问
  6. 复制令牌

3 — 将令牌保存到凭据文件:

bash

mkdir -p /etc/letsencrypt/cloudflare
nano /etc/letsencrypt/cloudflare/credentials.ini

粘贴:

dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN

锁定文件:

bash

chmod 600 /etc/letsencrypt/cloudflare/credentials.ini

4 — 请求证书:

使用您的管理员电子邮件和域名更新以下命令。

bash

certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
  --non-interactive \
  --agree-tos \
  --email youremailadress@domain.tld \
  -d mail.domain.tld

在您的结果中,应该有一条说明,内容如下:

Certbot has set up a scheduled task to automatically renew this certificate in the background.

Certbot 将设置一个 cron 任务,每天检查两次证书到期情况。如果证书在 30 天内到期,它将续订证书。您可以通过以下方式验证:

# 检查 systemd 计时器是否处于活动状态(大多数现代 Ubuntu 系统)
systemctl status certbot.timer

# 或检查是否添加了 cron 作业
cat /etc/cron.d/certbot

现在您的服务器上有了新邮件接收器域名的 TLS 证书。它们不在可以使用的位置。

5 — 设置部署脚本以移动文件
由于 certbot 会自动续订,您只需要脚本来处理 Discourse 特定的部分——复制已续订的证书并重建邮件接收器。您可以通过使用 certbot 内置的**部署挂钩(deploy hook)**来大大简化脚本,该挂钩在成功续订后自动运行。

创建一个部署挂钩文件:

bash

nano /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh
chmod +x /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh

粘贴以下内容:

bash

#!/bin/bash
DOMAIN="mail.domain.tld"
DISCOURSE_DIR="/var/discourse"
CERT_SRC="/etc/letsencrypt/live/${DOMAIN}"
CERT_DEST_1="${DISCOURSE_DIR}/shared/mail-receiver/letsencrypt/${DOMAIN}"
CERT_DEST_2="${DISCOURSE_DIR}/shared/mail-receiver/letsencrypt/${DOMAIN}_ecc"
ADMIN_EMAIL="admin email address"
LOG_FILE="/var/log/mail-cert-renewal.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "=== Certbot deploy hook triggered for ${DOMAIN} ==="

# 复制证书(使用 -L 解析符号链接)
for DEST in "$CERT_DEST_1" "$CERT_DEST_2"; do
    mkdir -p "$DEST"
    cp -L "${CERT_SRC}/fullchain.pem" "${DEST}/fullchain.pem"
    cp -L "${CERT_SRC}/privkey.pem"   "${DEST}/privkey.pem"
    cp -L "${CERT_SRC}/cert.pem"      "${DEST}/cert.pem"
    cp -L "${CERT_SRC}/chain.pem"     "${DEST}/chain.pem"
    chmod 644 "${DEST}/fullchain.pem" "${DEST}/cert.pem" "${DEST}/chain.pem"
    chmod 600 "${DEST}/privkey.pem"
    log "Certs copied to ${DEST}"
done

# 重建邮件接收器
cd "$DISCOURSE_DIR" || { echo "Cannot cd to ${DISCOURSE_DIR}" | mail -s "[FAILURE] Mail cert deploy hook failed" "$ADMIN_EMAIL"; exit 1; }
log "Rebuilding mail-receiver..."
if ./launcher rebuild mail-receiver >> "$LOG_FILE" 2>&1; then
    log "mail-receiver rebuilt successfully"
else
    log "ERROR: rebuild failed"
    echo "mail-receiver rebuild failed after cert renewal. Check ${LOG_FILE}" | \
        mail -s "[FAILURE] Mail cert deploy hook failed" "$ADMIN_EMAIL"
    exit 1
fi

log "=== Deploy hook completed successfully ==="

完全不需要手动设置 cron 任务——certbot 会协调整个过程。部署挂钩仅在实际发生续订时触发,因此在 certbot 检查但未续订的日子里,您的邮件接收器不会不必要地重建。

要测试续订挂钩,请运行以下命令:

bash

bash /etc/letsencrypt/renewal-hooks/deploy/mail-receiver-deploy.sh

如果一切设置正确,它将
→ 将证书复制到 Discourse 目录
→ 重建邮件接收器
→ 记录所有内容

6 — 在 mail-receiver.yml 中设置 TLS

这里的主要问题似乎是 forum.domain.tld 的 A 记录被代理隐藏了,而不是明确希望邮件服务器使用单独的域名。

在协商 TLS 时,会将证书的通用名称与 MX 记录的主机名进行比较,即客户端(可能是另一个邮件服务器)尝试连接的主机名,而不是它引用的 A 记录。 这意味着您可以将 mail.domain.tld 的 A 记录设置为“仅限 DNS 模式”,然后创建引用 mail.domain.tldforum.domain.tld 的 MX 记录,在这种安排下不再需要额外的特殊步骤。

是的,您可以对主论坛的 A 记录使用仅 DNS 模式。使用此方法意味着您会失去 CloudFlare 的反向全局代理功能。(这对于我的 Discourse 安装来说不是一个选项。)

这就是为什么第一行定义此解决方案适用于使用 CloudFlare 代理的网站的原因。

我指的是将 mail.domain.tld 的 A 记录设置为“仅限 DNS 模式”(DNS Mode Only),而不是 forum.domain.tld 的 A 记录,但我意识到我误解了 SMTP 客户端如何验证 TLS 证书。

我所看到的行为是默认的“机会主义”(opportunistic)方法的产物,该方法不验证主机名,因此我断言它验证 MX 记录的主机名而不是 MX 记录的目标是错误的。在大多数情况下它会起作用,但如果使用 DANE 或 MTA-STS 来强制执行 TLS 身份验证则不行。

将 A 记录设置为“代理”而 MX 记录设置为“仅限 DNS”是行不通的。CloudFlare 文档指出,任何 A 记录被代理的域名都会阻止所有 SMTP 流量。

我通过多轮测试验证了这一点。一旦取消 A 记录的代理,SMTP 数据就会流动。开启代理后,SMTP 数据就永远不会流动。(测试是通过使用 TELNET 连接到 25 端口进行的。)

因此,如果你想要:

  • 你的 Discourse 论坛使用 CloudFlare 代理服务
  • 你的 SMTP 邮件接收器接受邮件

那么你必须为你的入站邮件使用不同的域名。

如果你想为你的 SMTP 邮件接收器启用 TLS:

  • 你必须通过 DNS 验证设置 Let’s Encrypt

这些说明看起来令人生畏,但编写说明所花的时间比实际实施解决方案的时间要长。

我不是那个意思,我具体建议的是三条 DNS 记录:
A:forum.domain.tld → 主机 IP 地址(代理已启用)
A:mail.domain.tld → 主机 IP 地址(仅限 DNS 模式)
MX:forum.domain.tldmail.domain.tld

不过如前所述,我后来意识到这只在默认的“机会主义 TLS”模式下有效,如果你(某人)还想启用 DANE 或 MTA-STS 来强制身份验证(确保连接到正确的服务器而不是只加密流量),那将不起作用。

它们看起来非常好,易于遵循,并且在容器外部完成所有操作,因此没有可能因 Discourse 更新而破坏的风险。我特别喜欢使用 certbot 续订钩子,我以前不熟悉这个。

1 个赞

请注意,这会暴露您论坛的 IP 地址,并可能导致人们绕过 Cloudflare 的保护机制,例如 DDoS 防护和 WAF。最好在单独的服务器上运行邮件接收器。

我最初的意图是出于这个原因在另一台服务器上运行邮件接收器。每当我尝试运行启动器应用程序来启动邮件接收器时,它都想安装完整的 Discourse 系统。是否有简单的方法仅在独立的 Docker 服务器上启动和运行邮件接收器?

由于我们自己搭建基础设施,我不太熟悉启动器应用程序,但我的想法是不使用启动器应用程序,而是像启动其他容器一样启动它。您需要的环境变量在自述文件中。

@Simon_Manning@RGJ - Cloudflare 的说明文档已更新,提供了三个主要选项及其权衡。希望这能解决你们对最初提出的选项所提出的各种问题。

@kelv 你可以考虑在 CloudFlare 说明文档的主描述中添加一个脚注。这将节省相关用户的时间。Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver - #541 by LotusJeff

2 个赞