1. 为什么PAM不是“插件”,而是一套精密的权限调度中枢
很多人第一次听说Linux PAM(Pluggable Authentication Modules),下意识会把它理解成“认证插件集合”——就像浏览器装个广告屏蔽插件那样,加一个模块就多一项功能。这种认知偏差,恰恰是后续所有配置失效、策略失控、安全漏洞频发的根源。我带过三届运维新人培训,几乎每届都有人把/etc/pam.d/sshd文件当成“开关清单”,删掉一行觉得“少个验证更省事”,结果第二天SSH登录直接瘫痪,连root都进不去。这不是操作失误,而是对PAM本质的误读。
PAM真正的角色,是Linux内核与用户空间服务之间的一套策略仲裁层。它不直接处理密码比对、密钥校验或生物识别,而是决定“在什么时机、由谁、以何种顺序、依据哪条规则”去调用那些底层能力。你可以把它想象成一家24小时银行的智能柜台调度系统:ATM机(底层认证模块)本身具备取款、转账、查询功能,但真正决定“客户刷身份证后是否允许查余额”“输错三次密码后是否锁定账户”“VIP客户是否跳过二次验证”的,是后台那套实时响应、可动态更新、支持多策略并行的调度引擎——PAM就是这个引擎。
它的体系结构之所以值得“深入”,正在于其四层解耦设计:
- 应用接口层(Application Interface):
login、su、sshd等服务程序通过libpam.so调用统一API,完全不关心底层用的是LDAP还是本地shadow文件; - PAM核心层(PAM Library):
libpam库解析/etc/pam.d/下的策略文件,按类型(auth、account、password、session)分发请求; - 模块管理层(Module Management):每个
.so模块只专注一件事(如pam_unix.so管本地密码,pam_ldap.so管目录服务),模块间无直接调用; - 策略控制层(Control Flag Logic):
[success=ok default=ignore]这类控制标记,才是PAM的灵魂——它让同一模块在不同场景下产生截然不同的策略效果。
这解释了为什么网上大量“PAM教程”教你怎么加载pam_tally2.so防暴力破解,却没人告诉你:如果把它放在auth [default=die]位置,失败一次就终止整个认证链;而放在account [default=ok]位置,则仅用于账号状态检查,不影响密码验证流程。控制标记的语义,比模块本身更重要。我曾修复过一个生产环境故障:某次安全加固后,所有用户无法登录图形界面,排查三天才发现是/etc/pam.d/gdm-password里一句auth [success=done default=ignore] pam_succeed_if.so user ingroup nopasswdlogin被误删,导致非特权组用户被强制跳过后续认证步骤——表面看是“登录变快了”,实则是权限管控彻底失效。
关键词“Linux”“PAM”“体系结构”在此刻有了真实重量:它不是名词堆砌,而是指向一套必须理解其调度逻辑、控制流走向、模块协作边界的运行时架构。接下来,我们将撕开/etc/pam.d/目录的表象,直击PAM如何用四类模块、五种控制标记、三层调用栈,构建起Linux权限世界的交通管制系统。
2. 四类模块的本质分工:auth/account/password/session不是功能分类,而是策略断点
翻看/etc/pam.d/目录下的文件,你会发现每行开头都标着auth、account、password或session。绝大多数资料把它们解释为“认证模块”“账号管理模块”“密码修改模块”“会话管理模块”。这种说法没错,但严重误导了实操者——它让人误以为模块功能由类型决定,而忽略了PAM类型本质上是策略执行的四个关键断点(checkpoint),每个断点承载着不可替代的决策职责。
2.1 auth:身份声明的首次校验,而非最终确认
auth类型模块处理的是“你是谁”的初步声明。重点在于“声明”二字:它不负责最终裁决,只提供证据链中的第一环。例如pam_unix.so auth [success=ok default=ignore]这行,作用是读取/etc/shadow比对密码,但它的返回值只是“密码正确”或“密码错误”,并不决定“是否允许登录”。真正拍板的是后续account模块对账号状态的审查。
我遇到过最典型的误用案例:某金融客户要求“所有用户登录必须绑定U盾”,运维同事直接在/etc/pam.d/sshd的auth段加入pam_usb.so,结果导致所有SSH连接在密码验证后立即中断——因为pam_usb.so在auth阶段返回PAM_AUTH_ERR时,PAM默认终止整个认证链。正确做法是将其置于auth [success=ok default=bad]位置,并配合account段的pam_access.so做二次放行,确保U盾缺失时仍能走备用通道(如OTP)。auth断点的核心价值,在于为后续决策提供可信凭证,而非自行裁决。
2.2 account:账号生命周期的守门人,决定“你能否在此时此地使用系统”
如果说auth回答“你是不是这个人”,account则回答“这个人现在有没有资格使用本服务”。它检查的全是动态状态:账号是否过期(/etc/shadow的expire字段)、是否在指定时间段可用(pam_time.so)、是否属于允许登录的组(pam_access.so)、是否达到最大并发会话数(pam_limits.so)。
这里有个反直觉的设计:account模块可以拒绝auth已通过的用户。我们曾部署一套审计系统,要求开发人员只能在工作时间(8:00-18:00)通过跳板机访问生产库。同事在/etc/pam.d/sshd的account段配置:
account [success=ok default=bad] pam_time.so并在/etc/security/time.conf中写:
sshd;Al0000-2400;devgroup;!Al0800-1800意思是“sshd服务对devgroup组用户,禁止在8:00-18:00之外访问”。结果上线后,所有开发人员在非工作时间尝试登录时,系统直接返回Permission denied,连密码输入框都不出现。原因在于pam_time.so在account断点返回PAM_PERM_DENIED时,PAM直接终止流程,根本不会走到auth阶段。这恰恰证明了account的权威性——它拥有在身份确认前就否决访问的权利。
2.3 password:密码策略的执行引擎,与auth形成闭环验证
password类型常被误解为“改密码时才触发”,其实它是所有密码相关操作的统一入口。当用户执行passwd命令时,PAM按password段顺序调用模块:先由pam_pwquality.so检查新密码强度(长度、复杂度、历史记录),再由pam_unix.so执行实际的shadow文件更新。关键在于,password段与auth段存在隐式关联——pam_unix.so在auth段读取密码哈希,在password段则负责生成新哈希并写入。
这里埋着一个高危陷阱:若password段配置了pam_ldap.so但网络中断,passwd命令会卡住30秒(默认超时)。更糟的是,某些旧版pam_ldap.so在失败时返回PAM_IGNORE,导致密码修改看似成功,实则未同步到LDAP服务器。我们的解决方案是在password段采用双写策略:
password [success=ok default=ignore] pam_unix.so obscure sha512 password [success=ok default=bad] pam_ldap.so use_authtokuse_authtok参数强制pam_ldap.so跳过自身认证,直接使用pam_unix.so已验证的token更新密码,既保证本地密码即时生效,又通过default=bad确保LDAP失败时给出明确错误提示。
2.4 session:会话生命周期的编织者,从登录到登出的全程管控
session模块处理的是“登录后发生什么”,它不参与认证决策,却深刻影响用户体验与系统安全。典型应用包括:
pam_umask.so设置用户默认umask(如umask=002让组写权限生效);pam_env.so加载环境变量(如PATH=/usr/local/bin:/usr/bin);pam_exec.so执行自定义脚本(如登录时自动挂载家目录加密卷)。
最易被忽视的是session的双向性:它在登录时(open_session)和登出时(close_session)各执行一次。我们曾为某政务云平台实现“会话水印”功能——用户登录时,pam_exec.so调用脚本在终端背景绘制含IP、时间、用户名的半透明水印;登出时,同一脚本清除水印。若只配置session [success=ok default=ignore] pam_exec.so /path/to/watermark.sh,水印将永远残留。正确写法是显式声明:
session [success=ok default=ignore] pam_exec.so type=open_session /path/to/watermark.sh session [success=ok default=ignore] pam_exec.so type=close_session /path/to/cleanup.shtype参数确保脚本在正确时机触发,这是session模块区别于其他类型的核心特征。
提示:
session段的执行顺序直接影响环境初始化效果。例如pam_umask.so必须在pam_env.so之前加载,否则环境变量中的umask设置会被覆盖。PAM按文件中出现顺序执行,没有隐式依赖关系。
3. 控制标记的数学逻辑:success=ok default=ignore不是语法糖,而是状态机转移规则
PAM配置中最令人头疼的,莫过于那一长串[success=ok default=ignore]之类的控制标记。网上教程常将其简化为“成功就继续,失败就跳过”,这种解释在简单场景下勉强可用,一旦涉及多模块串联、条件跳转,就会引发灾难性误判。实际上,每个控制标记都是一个微型状态机的转移指令,其行为由模块返回值、控制标记类型、当前栈深度共同决定。
3.1 PAM返回值的六种语义:理解底层信号才能驾驭上层逻辑
所有PAM模块最终都返回六个标准值之一,它们构成状态机的输入信号:
| 返回值 | 语义 | 典型场景 |
|---|---|---|
PAM_SUCCESS | 操作完全成功 | pam_unix.so密码比对正确 |
PAM_IGNORE | 模块主动放弃,不参与决策 | pam_succeed_if.so条件不匹配时返回 |
PAM_AUTH_ERR | 认证失败 | 密码错误、证书过期 |
PAM_PERM_DENIED | 权限不足 | 账号被禁用、不在允许组 |
PAM_MAXTRIES | 尝试次数超限 | pam_faildelay.so触发 |
PAM_NEW_AUTHTOK_REQD | 需要更新凭证 | 密码过期强制修改 |
关键洞察在于:PAM_IGNORE和PAM_SUCCESS在策略效果上完全等价。很多管理员困惑“为什么pam_access.so拒绝访问却不报错”,正是因为pam_access.so在规则不匹配时返回PAM_IGNORE,而[default=ignore]标记恰好将PAM_IGNORE映射为“忽略本模块,继续执行下一条”。这解释了为何pam_access.so常被放在auth段末尾——它不改变认证结果,只作为兜底策略。
3.2 控制标记的四种模式:从线性执行到条件跳转
PAM定义了四种控制标记语法,每种对应不同的状态转移逻辑:
3.2.1 简单标记(Simple Flags)
required、requisite、sufficient、optional是最基础的四类。它们的区别在于对认证链的影响:
requisite:一旦返回非PAM_SUCCESS,立即终止整个认证链,返回对应错误;required:即使失败也继续执行后续模块,但最终结果必须全为PAM_SUCCESS才通过;sufficient:若返回PAM_SUCCESS,立即终止认证链并返回成功;optional:返回值不影响整体结果,仅用于日志记录。
实战中,requisite是安全加固的利器。例如在/etc/pam.d/common-auth中添加:
auth [user_unknown=ignore success=ok ignore=ignore default=bad] pam_faildelay.so delay=3000000这里user_unknown=ignore表示用户不存在时不触发延迟,default=bad确保其他失败情况均返回PAM_AUTH_ERR。配合requisite语义,任何认证失败都会立即终止,避免攻击者通过响应时间差异判断用户名是否存在。
3.2.2 值映射标记(Value Mapping)
[success=ok default=ignore]这类语法允许精确映射返回值到动作。ok表示“视为成功,继续执行下一条”,ignore表示“跳过本模块,继续执行下一条”,done表示“终止当前类型的所有模块”,bad表示“终止整个认证链”。
我们曾用此特性实现“双因子降级”:当Google Authenticator(pam_google_authenticator.so)不可用时,自动切换到短信验证码。配置如下:
auth [success=ok default=ignore] pam_google_authenticator.so auth [success=done default=ignore] pam_exec.so /usr/local/bin/fallback-sms.sh auth [default=bad] pam_deny.so逻辑是:若Google验证成功(success=ok),继续执行;若失败(default=ignore),跳过进入下一行;第二行中,若短信脚本返回成功(success=done),立即终止auth段,跳过pam_deny.so;若短信也失败,则执行最后一行pam_deny.so彻底拒绝。这种基于返回值的条件跳转,是PAM策略灵活性的基石。
3.2.3 跳转标记(Jump Flags)
[success=2 default=ignore]中的数字表示跳过后续几行模块。例如success=2意为“若成功,跳过接下来两行配置”。这在需要绕过特定模块时极为高效。
某次等保测评要求“SSH登录必须记录完整命令行”,但pam_exec.so执行脚本可能因超时阻塞登录。我们采用跳转方案:
auth [success=1 default=ignore] pam_exec.so /usr/local/bin/log-login.sh auth [default=ignore] pam_exec.so /usr/local/bin/log-login-fallback.sh auth [default=bad] pam_deny.so第一行若成功(success=1),跳过第二行直接执行第三行;若失败(default=ignore),执行第二行备用脚本;第三行确保最终有明确结果。跳转标记让配置具备了编程般的分支能力。
3.2.4 组合标记(Combined Flags)
[success=ok default=bad user_unknown=ignore]可同时处理多个返回值。这在复杂策略中不可或缺。例如限制root用户只能从特定IP登录:
auth [user_unknown=ignore success=ok default=bad] pam_access.so accessfile=/etc/security/access-limited.confaccess-limited.conf内容:
+ : root : 192.168.1.0/24 + : root : LOCAL - : root : ALLuser_unknown=ignore确保非root用户不受影响,default=bad让不匹配规则的root访问直接失败,success=ok则允许匹配用户继续认证流程。
注意:控制标记的解析顺序是自左向右,且
default是最后兜底项。若同时指定success=ok和default=bad,success优先级高于default,但仅对PAM_SUCCESS生效。
4. 策略文件的继承机制:/etc/pam.d/common-*不是模板,而是策略分发总线
初学者常把/etc/pam.d/common-auth这类文件当作“通用配置模板”,认为修改它就能一劳永逸。这种理解导致两个严重后果:一是策略冲突(如common-auth中启用pam_faildelay.so,而sshd单独配置又禁用,结果延迟失效);二是维护黑洞(某天发现sudo突然要求二次验证,排查半天才发现是common-auth里新增了一行pam_pwquality.so)。
实际上,common-*系列文件是Debian/Ubuntu系发行版实现的策略分发总线(Policy Distribution Bus)。它不提供功能,只承担策略路由职责。其核心机制是@include指令——每个服务配置文件(如/etc/pam.d/sshd)通过@include common-auth将common-auth的内容“内联展开”到自身配置中,形成最终执行链。
4.1 包含链的拓扑结构:理解谁调用谁才能避免策略污染
以Ubuntu 22.04的SSH登录为例,完整的包含链如下:
/etc/pam.d/sshd └── @include common-auth # 加载认证策略 └── @include common-account # 加载账号策略 └── @include common-password # 加载密码策略 └── @include common-session # 加载会话策略这意味着:
- 修改
common-auth会影响sshd、login、su、sudo等所有包含它的服务; common-account中的pam_time.so配置,会同时约束SSH登录、控制台登录、甚至cron作业的执行时段;common-session里pam_umask.so的设置,会让所有shell会话默认获得相同umask。
我们曾因此引发一次重大事故:为满足等保要求,在common-session中添加pam_umask.so umask=0077,意图让所有用户文件默认私有。结果第二天财务部门报告“ERP系统无法生成报表”,排查发现是报表服务以erpuser身份运行,其临时文件因umask=0077导致组内其他进程无法读取。根本原因是common-session的全局性——它不该承载服务级策略。
4.2 策略分层的最佳实践:服务专属配置 > 公共配置 > 内核默认
基于多年生产环境经验,我总结出PAM策略分层铁律:
4.2.1 服务专属配置(最高优先级)
每个服务(sshd、login、sudo)的配置文件应只包含该服务特有的策略。例如:
sshd中启用pam_google_authenticator.so(SSH特有双因子);sudo中配置pam_wheel.so group=admin(仅sudo需wheel组授权);login中设置pam_lastlog.so(控制台登录需记录最后登录时间)。
这些策略绝不放入common-*,避免跨服务污染。
4.2.2 公共配置(中优先级)
common-*文件只存放跨服务一致的基础策略,且必须满足两个条件:
- 无副作用:如
pam_env.so加载/etc/environment,所有服务都需要统一环境变量; - 可逆性强:如
pam_faildelay.so增加登录延迟,失败时不影响功能,仅提升安全性。
我们团队的标准common-auth精简到仅5行:
# /etc/pam.d/common-auth auth [success=ok default=ignore] pam_faildelay.so delay=3000000 auth [success=ok default=ignore] pam_permit.so auth [success=done default=ignore] pam_deny.so auth [default=bad] pam_deny.so @include common-local-auth前三行实现“延迟-许可-拒绝”的三段式认证框架,最后一行引入common-local-auth(本地策略),将具体模块(pam_unix.so、pam_ldap.so)隔离到独立文件,便于按需启停。
4.2.3 内核默认策略(最低优先级)
当某个服务配置文件中未定义某类模块(如sshd中无account段),PAM会回退到/etc/pam.d/other文件。该文件应保持极简,仅包含@include common-account,作为最后兜底。切忌在other中写具体策略,否则所有未显式配置的服务都将继承。
提示:使用
pam-config工具(SUSE系)或pam-auth-update(Debian系)可安全管理common-*文件,避免手动编辑导致语法错误。但工具生成的配置往往冗余,建议仅用作初始框架,后续全部手工精简。
5. 实战排障:从PAM_DEBUG日志到strace追踪,定位策略失效的完整链路
当PAM策略看似配置正确却无效时,90%的情况源于三个盲区:模块加载失败、控制标记语义误读、服务程序绕过PAM。我经历过最棘手的一次故障:某银行核心系统要求“所有数据库连接必须强制二次认证”,运维同事在/etc/pam.d/postgresql中配置了pam_google_authenticator.so,但测试时发现psql命令仍可免密登录。排查过程堪称PAM体系结构的全景解剖。
5.1 第一层:验证模块是否真实加载
首要怀疑是模块未加载。PAM提供PAM_DEBUG环境变量开启调试日志:
# 临时启用调试 export PAM_DEBUG=1 psql -U postgres日志输出关键行:
pam_sm_authenticate: called pam_google_authenticator: unable to open /home/postgres/.google_authenticator: No such file or directory问题浮出水面:pam_google_authenticator.so尝试读取/home/postgres/.google_authenticator,但PostgreSQL服务以postgres用户运行,其家目录是/var/lib/postgresql,而非/home/postgres。这是典型的模块路径假设错误——pam_google_authenticator.so默认在家目录找密钥文件,而数据库服务的家目录与普通用户不同。
解决方案是显式指定密钥路径:
auth [success=ok default=bad] pam_google_authenticator.so secret=/var/lib/postgresql/.google_authenticator5.2 第二层:确认服务程序是否真走PAM流程
即使模块加载成功,服务程序也可能绕过PAM。PostgreSQL支持多种认证方式,其pg_hba.conf文件中的local行配置为:
local all all peerpeer认证方式直接读取操作系统用户名,完全不调用PAM!这才是策略失效的根本原因。必须改为:
local all all pam并确保/etc/pg_hba.conf中pam_service_name指向正确的PAM服务名(如postgresql)。
5.3 第三层:用strace追踪系统调用,验证PAM调用链
当日志仍无法定位问题时,需深入系统调用层。使用strace跟踪psql进程:
strace -e trace=openat,open,connect,sendto,recvfrom -f psql -U postgres 2>&1 | grep -E "(pam|\.so)"输出显示:
[pid 12345] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/security/pam_google_authenticator.so", O_RDONLY|O_CLOEXEC) = 3 [pid 12345] sendto(3, "PAM_AUTHENTICATE\0", 19, MSG_NOSIGNAL, NULL, 0) = -1 ENOTCONNENOTCONN错误表明PAM模块加载成功,但在调用pam_authenticate()时连接异常。进一步检查发现,pam_google_authenticator.so依赖libqrencode.so生成二维码,而该库未安装。strace精准定位到缺失的动态链接库。
5.4 第四层:构建最小化复现环境,隔离干扰因素
为避免生产环境风险,我们搭建最小化复现环境:
# 创建测试用户 useradd -m -d /tmp/testuser testuser echo "testuser:password" | chpasswd # 编写最小PAM配置 cat > /etc/pam.d/testservice << 'EOF' auth [success=ok default=bad] pam_exec.so /bin/sh -c 'echo "PAM triggered" >> /tmp/pam-test.log; exit 0' EOF # 测试调用 LD_PRELOAD=/lib/x86_64-linux-gnu/libpam.so.0 \ pamtester testservice testuser authenticatepamtester是专为PAM调试设计的工具,它模拟服务程序调用PAM API,输出详细执行路径。当看到PAM triggered写入日志,证明PAM链路畅通;若失败,则pamtester会明确指出哪一行配置、哪个返回值导致中断。
这套四层排障法(日志→配置→系统调用→最小复现)覆盖了PAM失效的所有可能路径。它不仅是技术手段,更是对PAM体系结构的深度验证——每一层都在确认架构中某个关键组件是否按设计运转。
6. 安全加固的边界思考:PAM能做什么,不能做什么,以及为什么必须与其他机制协同
深入PAM体系结构的终极目的,不是为了炫技式配置,而是构建纵深防御体系。但必须清醒认识到:PAM是权限决策的“大脑”,却不是执行的“手脚”。它能决定“是否允许登录”,但无法阻止已登录用户执行rm -rf /;它能要求双因子认证,但无法防止用户将TOTP密钥截图存手机。我在金融行业十年安全实践中,见过太多因高估PAM能力而导致的防护失效。
6.1 PAM的能力边界:三类它无法解决的安全问题
6.1.1 内存泄露与侧信道攻击
PAM模块在用户空间运行,其内存可能被恶意程序通过ptrace或/proc/[pid]/mem读取。例如pam_unix.so在验证密码时,会将明文密码短暂存入内存。虽然现代模块采用mlock()锁定内存页,但仍有被coredump捕获的风险。解决方案是禁用coredump(ulimit -c 0)并启用KSM(Kernel Samepage Merging)内存去重。
6.1.2 内核级提权绕过
PAM运行在用户态,无法拦截内核模块的提权行为。某次攻防演练中,红队利用eBPF程序在sys_read系统调用处注入代码,绕过所有PAM会话检查,直接获取root shell。PAM对此完全无感知。必须配合SELinux或AppArmor进行内核级访问控制。
6.1.3 服务程序自身的逻辑漏洞
PAM只控制认证入口,不监管服务内部逻辑。PostgreSQL的pg_hba.conf若配置trust认证方式,PAM策略形同虚设;OpenSSH的PermitRootLogin yes允许root直接登录,PAM的auth required pam_deny.so也无法阻止。PAM是门禁系统,但门禁再严,也防不住主人自己开门放贼进来。
6.2 必须协同的三大机制:构建PAM为中心的防御同心圆
6.2.1 与SELinux/AppArmor协同:从“能否登录”到“登录后能做什么”
PAM决定用户能否进入系统,SELinux决定用户进入后能访问哪些资源。例如,即使PAM允许devuser登录,SELinux策略可限制其只能访问/var/www/html,无法执行/usr/bin/gcc。配置示例:
# SELinux策略:devuser只能运行httpd相关进程 semanage user -a -R "staff_r sysadm_r" devuser_u semanage fcontext -a -t httpd_sys_content_t "/var/www/html(/.*)?" restorecon -Rv /var/www/html此时devuser通过PAM登录后,ls /etc/shadow会返回Permission denied,因为SELinux拒绝了devuser_u:staff_r:staff_t对shadow_t类型的访问。
6.2.2 与auditd协同:从“策略执行”到“行为留痕”
PAM可记录认证事件(pam_tty_audit.so),但无法审计命令执行。需结合auditd:
# audit.rules中添加 -a always,exit -F arch=b64 -S execve -F uid!=0 -k user_commands -a always,exit -F arch=b32 -S execve -F uid!=0 -k user_commands这样,当用户执行sudo rm -rf /时,auditd生成日志包含完整命令行、父进程、终端信息,而PAM日志只记录“sudo认证成功”。二者结合,才能还原完整攻击链。
6.2.3 与systemd-logind协同:从“会话创建”到“会话生命周期管控”
pam_systemd.so模块负责与systemd-logind通信,但它不控制会话销毁。我们曾为某政务系统实现“闲置15分钟自动锁屏”,仅靠PAM的session段无法实现,必须配置/etc/systemd/logind.conf:
IdleAction=lock IdleActionSec=900systemd-logind检测到终端无输入900秒后,向所有会话发送LockSession信号,PAM的pam_systemd.so再响应此信号执行锁屏操作。这是典型的“PAM提供策略接口,systemd提供执行引擎”的协同范式。
最后分享一个血泪教训:某次等保整改中,安全团队要求“所有用户密码必须90天更换”。运维同事在
/etc/pam.d/common-password中配置pam_pwquality.so maxage=90,结果导致所有用户第二天无法登录,因为maxage参数实际作用于/etc/shadow的max字段,而该字段需配合chage -M 90 username命令写入。PAM模块本身不修改shadow文件,它只在passwd命令执行时检查该字段。永远记住:PAM是策略引擎,不是执行代理。