跳转到主要内容

于 2025年04月22日 摘录自 Postfix SMTP Access Policy Delegation

Postfix SMTP 访问策略委托的目的

Postfix SMTP 服务器内置了多种机制,可在特定 SMTP 协议阶段阻止或接受邮件。此外,Postfix SMTP 服务器可将决策委托给外部策略服务器(Postfix 2.1 及更高版本)。

借助此策略委托机制,只需几行 Perl 代码即可实现简单的 greylist 策略,如本文末尾所示。完整示例可在 Postfix 源代码的 examples/smtpd-policy 目录中找到。

另一个策略委托示例是位于 https://web.archive.org/web/20190221142057/http://www.openspf.org/Software 的 SPF 策略服务器。

政策委托现已成为向 Postfix 添加政策的首选方法。使用 Perl、Python、Ruby 或 TCL 编写几行代码开发新功能,比用 C 代码实现相同功能要简单得多。性能差异在绝大多数环境下均不可察觉,仅在极端要求高的环境下可能有所体现。在活跃系统中,策略守护进程会根据 $max_use 最大入站 SMTP 连接数启动多个实例。

本文档涵盖以下主题:

协议描述

Postfix 策略委托协议非常简单。客户端发送请求,服务器发送响应。除非发生错误,否则服务器不得关闭连接,以便同一连接可多次使用。

客户端请求是一系列以换行符分隔的属性=值对,并以空行结束。服务器响应是一个属性=值对,同样以空行结束。

以下是 Postfix SMTP 服务器在委托 SMTPD 访问策略请求中发送的所有属性的示例:

Postfix 版本 2.1 及更高版本:
request=smtpd_access_policy
protocol_state=RCPT
protocol_name=SMTP
helo_name=some.domain.tld
queue_id=8045F2AB23
[email protected]
[email protected]
recipient_count=0
client_address=1.2.3.4
client_name=another.domain.tld
reverse_client_name=another.domain.tld
instance=123.456.7
Postfix 版本 2.2 及更高版本:
sasl_method=plain
sasl_username=you
sasl_sender=
size=12345
ccert_subject=solaris9.porcupine.org
ccert_issuer=Wietse+20Venema
ccert_fingerprint=C2:9D:F4:87:71:73:73:D9:18:E7:C2:F3:C1:DA:6E:04
Postfix 版本 2.3 及更高版本:
encryption_protocol=TLSv1/SSLv3
encryption_cipher=DHE-RSA-AES256-SHA
encryption_keysize=256
etrn_domain=
Postfix 版本 2.5 及更高版本:
stress=
Postfix 版本 2.9 及更高版本:
ccert_pubkey_fingerprint=68:B3:29:DA:98:93:E3:40:99:C7:D8:AD:5C:B9:C9:40
Postfix 版本 3.0 及更高版本:
client_port=1234
Postfix 版本 3.1 及更高版本:
policy_context=submission
Postfix 版本 3.2 及更高版本:
server_address=10.3.2.1
server_port=54321
Postfix 版本 3.8 及更高版本:
compatibility_level=major.minor.patch
mail_version=3.8.0
[空行]

注释:

  • "request"属性是必填的。在此示例中,请求类型为"smtpd_access_policy"。
  • 属性的顺序无关紧要。策略服务器应忽略其不关心的任何属性。
  • 当同一属性名称被多次发送时,服务器可能会保留第一个值或最后一个属性值。
  • 当属性值不可用时,客户端要么不发送该属性,要么发送空值("name="),或者在数值属性情况下发送零值("name=0")。
  • "recipient"属性在"RCPT TO"阶段可用。如果 Postfix 仅接受当前消息的一个收件人,该属性在"DATA"和"END-OF-MESSAGE"阶段也可用。DATA 协议状态也适用于通过 BDAT 命令接收的电子邮件(Postfix 3.4 及更高版本)。
  • "recipient_count"属性(Postfix 2.3 及更高版本)仅在"DATA"和"END-OF-MESSAGE"阶段为非零值。它指定 Postfix 接受的当前消息的收件人数。DATA 协议状态也适用于通过 BDAT 命令接收的电子邮件(Postfix 3.4 及更高版本)。
  • 远程客户端或本地服务器 IP 地址是 IPv4 格式(如 1.2.3.4)或 IPv6 格式(如 1:2:3::4:5:6)。
  • 远程客户端或本地服务器端口是 0 到 65535 之间的十进制数。
  • 有关反向和验证客户端名称信息的差异,请参阅 reject_unknown_client_hostname 部分。
  • 属性名称不得包含 "="、空字符或换行符,属性值不得包含空字符或换行符。
  • "instance" 属性值可用于关联同一消息投递的不同请求。这些请求通过同一策略连接发送(除非策略守护进程终止连接)。一旦 Postfix 通过同一策略连接发送包含不同 instance 属性的查询,之前的邮件投递将被完成或取消。
  • "size" 属性值指定客户端在 MAIL FROM 命令中指定的邮件大小(若未指定则为零)。在 Postfix 2.2 及更高版本中,它指定客户端发送 END-OF-MESSAGE 后实际的邮件大小。
  • "sasl_*"属性(Postfix 2.2 及更高版本)指定客户端通过 SASL 进行身份验证的相关信息。若未进行 SASL 身份验证,这些属性为空。
  • "ccert_*"属性(Postfix 2.2 及更高版本)指定客户端通过 TLS 进行身份验证的相关信息。若未进行证书身份验证,这些属性为空。从 Postfix 2.2.11 开始,这些属性值以 xtext 格式编码:某些字符由 +XX 表示,其中 XX 是该字符值的两位十六进制表示。在 Postfix 2.6 及更高版本中,解码后的字符串为不包含非打印 ASCII 字符的 UTF-8 字符串。
  • "encryption_*"属性(Postfix 2.3 及更高版本)指定连接加密方式的相关信息。对于明文连接,协议和密码属性为空,密钥长度为零。
  • "etrn_domain"属性仅在 ETRN 命令上下文中定义,用于指定 ETRN 命令的参数。
  • "stress"属性为空或"yes"。有关详细信息,请参阅STRESS_README文档。
  • "policy_context"属性提供了一种传递其他属性无法提供的信息的方式(Postfix 3.1 及更高版本)。
  • "compatibility_level"属性对应于compatibility_level参数的值。它采用以下形式:major.minor.patch,其中 minorpatch 可省略。
  • "mail_version"属性对应于mail_version参数的值。其格式为稳定版本的major.minor.patch,以及不稳定版本的major.minor-yyyymmdd

以下内容特定于 SMTPD 委托策略请求:

  • 协议名称为 ESMTP 或 SMTP。
  • 协议状态包括 CONNECT、EHLO、HELO、MAIL、RCPT、DATA、END-OF-MESSAGE、VRFY 或 ETRN;这些是 Postfix SMTP 服务器根据 SMTP 协议状态做出 OK/REJECT/HOLD 等决策的状态。DATA 协议状态也适用于通过 BDAT 命令接收的电子邮件(Postfix 3.4 及更高版本)。

策略服务器将返回 Postfix SMTPD access(5) 表中允许的任何操作。示例:

action=defer_if_permit 服务暂时不可用
[空行]

这将导致 Postfix SMTP 服务器以 450 暂时错误代码和文本"服务暂时不可用"拒绝请求,如果 Postfix SMTP 服务器未找到永久拒绝请求的理由。

在出现问题时,策略服务器不得发送回复。相反,服务器必须记录警告并断开连接。Postfix 将稍后重新尝试请求。

简单策略客户端/服务器配置

Postfix 委托策略客户端可以连接到 TCP 套接字或 UNIX 域套接字。示例:

inet:127.0.0.1:9998
unix:/some/where/policy
unix:private/policy

第一个示例指定策略服务器监听 TCP 套接字 127.0.0.1 端口 9998。第二个示例指定 UNIX 域套接字的绝对路径名。第三个示例指定相对于 Postfix 队列目录的路径名;此选项适用于由 Postfix 主守护进程启动的策略服务器。在许多系统中,"local"是"unix"的同义词。

要创建一个监听名为"policy"的UNIX域套接字并由Postfix spawn(8) 守护进程控制的策略服务,您可以使用类似以下内容:

1 /etc/postfix/master.cf:
2 policy unix - n n - 0 spawn
3 user=nobody argv=/some/where/policy-server
4 
5 /etc/postfix/main.cf:
6 smtpd_recipient_restrictions =
7 ... 
8 reject_unauth_destination 
9 check_policy_service unix:private/policy 
10 ...
11 policy_time_limit = 3600
12 # smtpd_policy_service_request_limit = 1

备注:

  • 第2-3行:这创建了一个名为"policy"的服务,该服务监听UNIX域套接字。该服务由 Postfix 的 spawn(8) 守护进程实现,该守护进程使用 argv 属性指定的参数执行政策服务器程序,并使用 user 属性指定的权限运行。
  • 第 2 行:将进程限制指定为 "0" 而不是默认的 "-",以避免在增加 smtpd 进程限制时出现 "连接被拒绝" 等问题。
  • 第 8 行:如果邮件中继策略已通过 reject_unauth_destination 指定,则此处无需指定。 html#smtpd_relay_restrictions">smtpd_relay_restrictions(Postfix 2.10 及更高版本可用)。
  • 第 8 行和第 9 行:始终在 " 在 "reject_unauth_destination" 之后,否则您的系统可能会成为开放中继。
  • 第 11 行:这将政策服务器进程的运行时间延长至 3600 秒。默认的 1000 秒时间限制过短;政策守护进程需要与与其通信的 SMTP 服务器进程同时运行。有关 spawn(8) 手册页中 transport_time_limit 参数的更多信息,请参阅该手册页。

    注意:在 Postfix 2.9 版本之前,"policy_time_limit" 参数不会在 "postconf" 命令输出中显示。此限制适用于许多参数,其名称由 master.cf 服务名称(在上例中为 "policy")和内置后缀(在上例中为 "_time_limit")组合而成。

  • 第 12 行:指定 smtpd_policy_service_request_limit 以避免与无法维持持久连接的策略服务器发生错误恢复延迟。
  • 对于 Solaris 版本小于 9,或任何 Solaris 版本上的 Postfix 版本小于 2.10,请使用 TCP 套接字代替 UNIX 域套接字:
 1 /etc/postfix/master.cf:
2 127.0.0.1:9998 inet n n n - 0 spawn
3 user=nobody argv=/some/where/policy-server
4
5 /etc/postfix/main.cf:
6 smtpd_recipient_restrictions =
7 ... 
8 reject_unauth_destination 
9 check_policy_service inet:127.0.0.1:9998
10 ...
11 127.0.0.1:9998_time_limit = 3600
12 # smtpd_policy_service_request_limit = 1

控制策略委托协议客户端侧的配置参数:

  • smtpd_policy_service_default_action (默认值:451 4.3.5 服务器配置问题):当 SMTPD 策略服务请求失败时的默认操作。自 Postfix 3.0 及更高版本起可用。
  • smtpd_policy_service_max_idle (默认值:300 秒):Postfix SMTP 服务器在关闭未使用的策略客户端连接前等待的时间。
  • smtpd_policy_service_max_ttl(默认:1000 秒):Postfix SMTP 服务器关闭活动策略客户端连接前的等待时间。
  • smtpd_policy_service_request_limit (默认值:0):每个策略连接的最大请求数,或 0(无限制)。适用于 Postfix 3.0 及更高版本。
  • smtpd_policy_service_timeout (默认:100 秒):连接到策略服务器、向策略服务器发送或从策略服务器接收数据的时间限制。
  • smtpd_policy_service_try_limit(默认:2):在放弃之前尝试发送 SMTPD 策略服务请求的最大次数。适用于 Postfix 3.0 及更高版本。
  • smtpd_policy_service_retry_delay (默认值:1 秒):重新发送失败的 SMTPD 策略服务请求之间的延迟时间。适用于 Postfix 3.0 及更高版本。
  • smtpd_policy_service_policy_context (默认值:空): 可选信息,通过 SMTPD 策略服务请求的 "policy_context" 属性传递(最初用于在多个 check_policy_service 客户端之间共享相同的 SMTPD 服务端点)。适用于 Postfix 3.1 及更高版本。

控制策略委托协议服务器端的配置参数:

  • transport_time_limit ($command_time_limit): 策略守护进程在被终止前允许运行的最大时间。transport 是策略守护进程服务在 master.cf 文件中的服务名称。在上述示例中,服务名称为 "policy" 或 "127.0.0.1:9998"。

高级策略客户端配置

前一节列出了 Postfix main.cf 文件中用于控制所有策略客户端的时间限制和其他设置的参数。对于简单配置,这已足够。在更复杂的配置中,为每个策略客户端设置不同参数变得必要。此功能在 Postfix 3.0 及更高版本中得到支持。

以下示例展示了一个"非关键"策略服务,其超时时间较短,且当服务不可用时默认采取"DUNNO"操作。 "DUNNO"操作会导致 Postfix 忽略结果。

1 /etc/postfix/main.cf:
2 mua_recipient_restrictions = 
3 ...
4 reject_unauth_destination
5 check_policy_service { inet:host:port,
6 timeout=10s, default_action=DUNNO
7 policy_context=submission }
8 ...

现在,我们不再使用服务器端点,而是使用用{}包围的列表。

  • 第5行:列表中的第一个项是服务器端点。它支持与之前描述的完全相同的"inet"和"unix"语法。
  • 第6-7行:列表的其余部分包含针对每个客户端的设置。这些设置会覆盖全局main.cf参数,且名称与这些参数相同,但不带"smtpd_policy_service_"前缀。

在列表内部,语法与我们之前在main.cf中看到的类似:项目用空格或逗号分隔。有一个区别:如果你希望在值中或"="周围包含空格或逗号,必须将设置用大括号括起来,如"{ name = value }"。这在不同策略服务器需要根据不同的 SMTP 状态码或文本采取不同默认操作时非常有用:

1 /etc/postfix/main.cf:
2 smtpd_recipient_restrictions =
3 ...
4 reject_unauth_destination
5 check_policy_service {
6 inet:主机:端口1,
7 { default_action = 451 4.3.5 参见 http://www.example.com/support1 }
8 } 
9 ...

示例:灰名单策略服务器

灰名单是一种防范垃圾邮件的防御机制,详情请参阅 http://www.greylisting.org/。该想法在 postfix-users 邮件列表中 在流行之前一年 就被讨论过。

Postfix 源代码树中的 examples/smtpd-policy/greylist.pl 文件实现了一个简化的灰名单策略服务器。该服务器为每个 (客户端、发件人、收件人) 三元组存储一个时间戳。默认情况下,邮件在时间戳超过 60 秒后才被接受。这阻止了使用随机选取的发件人地址发送的垃圾邮件,以及通过随机选取的开放代理发送的邮件。它还阻止了频繁更改 IP 地址的垃圾邮件发送者发送的邮件。

将 examples/smtpd-policy/greylist.pl 复制到 /usr/libexec/postfix 或系统中合适的位置。

在 greylist.pl Perl 脚本中,需要指定灰名单数据库文件的位置,以及邮件被接受前的延迟时间。默认设置为:

$database_name="/var/mta/greylist.db";
$greylist_delay=60;

/var/mta 目录(或您选择的其他目录)应可由 "nobody" 用户写入,或由您在 master.cf 中为策略服务配置的用户写入。

示例:

# mkdir /var/mta
# chown nobody /var/mta

注意:请勿将灰名单数据库创建在可供所有人写入的目录(如 /tmp 或 /var/tmp)中,也请勿将灰名单数据库创建在可能空间不足的文件系统中。Postfix 可在邮件队列和邮件箱存储存在空间不足的情况下继续运行,但无法在灰名单数据库损坏时继续运行。若文件损坏,您可能无法接收任何邮件,直至手动删除该文件。

greylist.pl Perl 脚本可在 Postfix 主守护进程的控制下运行。例如,以用户 "nobody" 身份运行脚本,并使用仅对 Postfix 进程可访问的 UNIX 域套接字:

 1 /etc/postfix/master.cf:
2 greylist unix - n n - 0 spawn
3 user=nobody argv=/usr/bin/perl /usr/libexec/postfix/greylist.pl
4 
5 /etc/postfix/main.cf:
6 greylist_time_limit = 3600
7 smtpd_recipient_restrictions =
8 ... 
9 reject_unauth_destination 
10 check_policy_service unix:private/greylist
11 ...
12 # smtpd_policy_service_request_limit = 1

注释:

  • 第2-3行:这创建了一个名为"greylist"的服务,该服务监听UNIX域套接字。该服务由 Postfix 的 spawn(8) 守护进程实现,该守护进程使用 argv 属性指定的参数执行 greylist.pl 脚本,并使用 user 属性指定的权限运行。
  • 第 2 行:将进程限制指定为 "0" 而不是默认的 "-",以避免在增加 smtpd 进程限制时出现 "连接被拒绝" 等问题。
  • 第 3 行:指定 "greylist.pl -v" 以详细记录每个请求和响应。
  • 第 6 行:将灰名单服务器进程的运行时间限制增加到 3600 秒。默认的 1000 秒时间限制过短;灰名单守护进程需要运行与与其通信的 SMTP 服务器进程相同的时间。有关spawn(8)手册页中transport_time_limit参数的更多信息,请参阅该手册页。
  • 第 9 行:reject_unauth_destination 在此处无需指定,若邮件中继策略已通过 smtpd_relay_restrictions(Postfix 2.10 及更高版本可用)。

    注意:在 Postfix 2.9 版本之前,"greylist_time_limit" 参数不会在 "postconf" 命令输出中显示。此限制适用于许多参数,其名称由 master.cf 服务名称(如上例中的 "greylist")和内置后缀(如上例中的 "_time_limit")组合而成。

  • 第 12 行:指定 smtpd_policy_service_request_limit 以避免与无法维持持久连接的策略服务器之间的错误恢复延迟。

在 Solaris < 9 或任何 Solaris 版本上的 Postfix < 2.10 中,请使用 inet 风格的套接字而非 unix 风格,具体详见上文"策略客户端/服务器配置"部分。

 1 /etc/postfix/master.cf:
2 127.0.0.1:9998 inet n n n - 0 spawn
3 user=nobody argv=/usr/bin/perl /usr/libexec/postfix/greylist.pl
4
5 /etc/postfix/main.cf:
6 127.0.0.1:9998_time_limit = 3600
7 smtpd_recipient_restrictions =
8 ... 
9 reject_unauth_destination 
10 check_policy_service inet:127.0.0.1:9998
11 ...
12 # smtpd_policy_service_request_limit = 1

对频繁伪造域名的邮件启用灰名单

对于经常出现在伪造邮件中的特定域名,启用灰名单相对安全。在网络空间/时间的某个时刻,可以在https://web.archive.org/web/20080526153208/http://www.monkeys.com/anti-spam/filtering/sender-domain-validate.in

 1 /etc/postfix/main.cf:
2 smtpd_recipient_restrictions =
3 reject_unlisted_recipient
4 ...
5 reject_unauth_destination 
6 check_sender_access hash:/etc/postfix/sender_access
7 ...
8 smtpd_restriction_classes = greylist
9 greylist = check_policy_service unix:private/greylist
10
11 /etc/postfix/sender_access:
12 aol.com greylist
13 hotmail.com greylist
14 bigfoot.com greylist
15 ... 等等 ...

备注:

  • 第9行:在Solaris < 9或任何Solaris版本上的Postfix < 2.10中,请使用inet:风格的套接字代替unix:风格,具体请参见上文"示例:greylist策略服务器"部分。
  • 第 5 行:如果邮件中继策略已通过 reject_unauth_destination 指定,则此处无需指定。 html#smtpd_relay_restrictions">smtpd_relay_restrictions(Postfix 2.10 及更高版本可用)。
  • 第 6 行:请确保在 "reject_unauth_destination" 在 "reject_unauth_destination" 之后,否则您的系统可能会成为开放邮件中继。
  • 第 3 行:在 Postfix 2.0 快照版本中,"reject_unlisted_recipient" 已更名为 "check_recipient_maps"。Postfix 2.1 同时支持两种形式。
  • 第 3 行:灰名单数据库会迅速被无效地址污染。建议通过其他限制措施(如拒绝未知发件人或收件人)来保护灰名单查询。

对所有邮件启用灰名单

如果您为所有邮件启用灰名单,可能需要为使用一次性发件人地址的邮件列表设置例外,因为每个邮件都会因灰名单而延迟,而一次性发件人地址会相对快速地污染灰名单数据库。 替代方案是自动将多次通过灰名单的客户端加入白名单;这可避免大部分延迟和数据库污染问题。

 1 /etc/postfix/main.cf:
2 smtpd_recipient_restrictions =
3 reject_unlisted_recipient
4 ...
5 reject_unauth_destination 
6 check_sender_access hash:/etc/postfix/sender_access
7 check_policy_service unix:private/policy
8 ...
9
10 /etc/postfix/sender_access:
11 securityfocus.com OK
12 ...

备注:

  • 第 7 行:在 Solaris 9 及以下版本,或任何 Solaris 版本上的 Postfix 2.10 及以下版本中,请使用 inet: 风格的套接字而非 unix: 风格,具体详见上文"示例:灰名单策略服务器"部分。
  • 第 5 行:如果邮件中继策略已通过 reject_unauth_destination 指定,则此处无需指定。 html#smtpd_relay_restrictions">smtpd_relay_restrictions(Postfix 2.10 及更高版本可用)。
  • 第6-7行:请确保指定check_sender_accesscheck_policy_servicereject_unauth_destination 之后,否则您的系统可能会成为开放邮件中继。
  • 第 3 行:灰名单数据库会迅速被无效地址污染。建议在灰名单查询前添加限制,拒绝未知发件人和/或收件人。

常规灰名单维护

灰名单数据库会随时间增长,因为灰名单服务器不会自动删除数据库条目。若不进行维护,灰名单数据库最终会耗尽文件系统空间。

当状态文件大小超过某个阈值时,您可以简单地重命名或删除该文件而不会产生不良影响;Postfix 会自动创建一个新文件。在最坏情况下,新邮件可能会延迟约一小时。为了减轻影响,建议在周末开始时在深夜重命名或删除该文件。

示例 Perl 灰名单服务器

这是实现示例灰名单策略的 Perl 子例程。它是随 Postfix 源代码分发的通用示例策略服务器的一部分,位于 examples/smtpd-policy/greylist.pl。

#
# 灰名单状态数据库和灰名单时间间隔。切勿将
# 灰名单状态数据库。
# 不要在可能用完空间的文件系统中创建灰名单数据库。
#
$database_name="/var/mta/greylist.db";
$greylist_delay=60;
#
# 自动允许列表阈值。设置为 0 禁用,或指定成功"返回"的次数
# 后,客户端不再受灰名单限制。
#
#
$auto_allowlist_threshold = 10;
#
# 演示 SMTPD 访问策略例程。结果与在 Postfix 访问策略表右侧指定的操作相同
# 请求属性可通过 %attr 哈希访问。
#
sub smtpd_access_policy {
my($key, $time_stamp, $now);
# 动态打开数据库。
open_database() unless $database_obj;
# 搜索自动允许列表。
if ($auto_allowlist_threshold > 0) {
$count = read_database($attr{"client_address"});
if ($count > $auto_allowlist_threshold) {
return "dunno";
}
}
# 查找此客户端/发件人/收件人的时间戳。
$key =
lc $attr{"client_address"}."/".$attr{"sender"}."/".$attr{"recipient"};
$time_stamp = read_database($key);
$now = time();
# 如果为新请求,将此客户端/发送者/接收者添加到数据库。
if ($time_stamp == 0) {
$time_stamp = $now;
update_database($key, $time_stamp);
}
# 结果可以是 Postfix access(5) 映射中允许的任何操作。
#
# 为了标记邮件,返回 ``PREPEND headername: headertext''
#
# 如果成功,返回 ``DUNNO'' 而不是 ``OK'',以便
# check_policy_service 限制可被其他限制跟随。
#
# 失败时返回 ``DEFER_IF_PERMIT 可选文本...'',
# 使邮件仍可被其他访问限制阻挡。
#
syslog $syslog_priority, "请求年龄 %d", $now - $time_stamp if $verbose;
if ($now - $time_stamp > $greylist_delay) {
# 更新自动允许列表。
if ($auto_allowlist_threshold > 0) {
update_database($attr{"client_address"}, $count + 1);
}
return "dunno";
} else {
return "defer_if_permit 服务暂时不可用";
}
}