四个人共用一台服务器,某天下午 Nginx 的配置被人改了,一个 location 段被注释掉,业务半边挂了半小时。翻登录日志,只有一条 root 从内网地址登录的记录;翻命令历史,只有几条谁都能敲的常规命令;问一圈,四个人都说不是自己。问题不在于有人撒谎,而在于这台机器的权限模型从头到尾就没有能力回答"是谁"这个问题——所有人都是用同一个身份进去的,机器记录了动作,没有记录人。
这类事情在小团队里特别常见,因为三五个人管几台机器的规模,正好卡在一个尴尬的位置:说大不大,没人愿意为它上一套集中式的权限系统;说小也不小,共用已经真实发生了,出事需要追责。于是最常见的处理方式就是发一个 root 密码或者灌一把共用的密钥,先用起来再说。这个选择在第一天没有任何代价,代价全部发生在出事那天,而且是追溯不回来的那部分代价。
下面按一条链路往下讲:为什么"说不清是谁"是模型缺陷而不是人的问题;把权限治理拆成授权粒度、凭据生命周期、可审计性三层之后分别要做什么;每一层单独做会怎么失效;三层之间怎么互相验证才能确认它们真的都成立;按角色分组和应急通道怎么留;以及什么时候逐台维护不再划算、该换集中式方案。本文只讲服务器本机这一层的事——账号、SSH 的认证方式、sudo 的授权写法、以及哪些动作必须留痕。堡垒机的产品选型与部署、日志采集管线与留存成本、等保测评流程,都不在本文范围内。
先把一个判断摆清楚:多人共用一台服务器出事之后追不到人,绝大多数情况下不是因为有人故意掩盖,而是因为这台机器在设计上就没有"人"这个维度。所有人都以 root 身份操作,系统记录的是"root 于某时执行了某命令",这是一条语法完整、信息为零的记录。它回答了"发生了什么",没有回答"谁做的",而后者才是追责和复盘真正需要的那个答案。
把视角再往前挪一点会发现,问题甚至不在"追责"这一刻,而在每一次操作发生之前。当一个人知道自己的动作不会留名时,他执行高风险命令的心理门槛会明显下降——不是说他一定会乱来,而是"反正查不到我"这个前提会改变人的行为。反过来,当每个人都知道自己的每个提权动作都能被定位到自己身上时,很多误操作在敲下回车之前就被自己拦下来了。可追溯性不只是事后工具,它对操作行为本身有约束力,这一点经常被低估。
还有一个更现实的影响:追不到人的时候,团队只能靠"集体担责"来收场,而集体担责等于无人担责,最后的结果往往是所有人一起被收紧权限,日常运维效率下降,然后过几个月又有人偷偷把共用的 root 密码拿出来用。这个循环我在小团队里见过太多次。打破它的办法不是加强教育,是把权限模型改掉,让"是谁"这个问题在系统层面就有答案。
所以本文讨论的"分权限",目标不是把权限管得更死,而是让三个问题在任何时刻都能被回答:这个人现在能做什么(授权粒度)、他凭什么能进来以及这个凭据什么时候失效(凭据生命周期)、他做过什么能被还原出来(可审计性)。这三件事任意一件答不上来,这台机器的权限治理就是断的。
很多人整理服务器权限时的思路是"找个最强的手段",比如上密钥、比如禁 root、比如开日志。这些手段单个看都对,但它们分属三个不同的层,解决的是三个不同的问题,互相之间不能替代。把它们放在一张图上,层与层之间是串联关系——任意一层断了,整条链就断了。
凭据层,管的是"凭什么能进来"。SSH 用口令还是用密钥、root 能不能远程登录、一把私钥能用到什么时候。这一层控制的是进入通道本身,它的生命周期属性最强——人会入职、会离职、会换电脑、会丢手机,凭据必须跟着这些事件变化。这一层做不好,后面两层做得再细也是白搭,因为门本来就是敞开的。
授权层,管的是"进来之后能做什么"。在 Linux 上主要就是 sudo 的规则:这个账号能执行哪些命令、要不要再输一次密码、以谁的身份执行。这一层的核心变量是命令粒度,而不是"给不给"。把授权理解成开关的人是做不好这一层的,因为现实里几乎所有合理的运维需求都需要一部分 root 能力,一刀切的结果是要么全给、要么天天找人代劳,两种结局都指向退化成共用 root。
留痕层,管的是"做过什么能还原出来"。登录与登出、提权执行、权限配置的变更、公钥的增删,这些动作是否产生记录,记录在哪里,本机被攻陷之后还剩下什么。这一层的作用是让前两层在事后可验证——授权写得再漂亮,如果事后查不出谁用了它,那这份授权就只是一张纸。
三层缺一层的后果是具体的,不是抽象的。缺凭据生命周期:人走了密钥还在机器上,他能进来,而且进来之后的动作看起来和在职时一样合法。缺授权粒度:所有人进来都是完整 root,任何误操作的影响面都是全机。缺可审计性:出了事只有结果没有过程,三层里的另外两层做得再好,你也只能知道"有权限的人里有一个人做了这件事"。所以本文的核心论点就是这一句——授权粒度、凭据生命周期、可审计性这三层必须同时成立,只做其中一层等于没做。给了 sudo 不留痕,事后说不清;留了痕但所有人用同一个账号,痕迹没有归属。
下面这张表把三层放在一起对照,最后一行加了"共享账号"这个反例——它不属于任何一层,但它能让前面三层的努力全部归零。
| 层级 | 控制的对象 | 常见做法 | 单独使用时的失效场景 | 与其他层的验证关系 |
|---|---|---|---|---|
| 凭据层(SSH 密钥与登录) | 谁能通过建立连接的身份认证,以及这个身份的有效期 | 禁止 root 远程登录;改用密钥认证并关闭口令登录;公钥带注释以便辨认归属 | 人员离职、设备更换后公钥未清理,权限长期滞留;私钥落在共享机器上后所有人都能冒用 | 登录记录里的账号,要能与授权层里该账号的 sudo 范围对得上;对不上说明存在未纳管账号 |
| 授权层(sudo 命令粒度) | 登录成功之后能以什么身份执行哪些命令 | 按角色分组授权;用命令别名限定到具体服务与具体参数;用 visudo 校验语法后再生效 | 规则里出现通配符或可逃逸程序,名义上开了一条命令,实际等于给了完整 shell | 实际执行的命令必须落在授权清单之内;出现了清单外的命令,说明规则被改过或写得过宽 |
| 留痕层(操作记录) | 登录、提权、权限配置变更这些动作是否被记录下来 | 开启 sudo 调用日志;把记录转发到本机之外的位置;时间统一同步 | 日志只写在本机,机器被攻陷后本机日志可被改写或删除,事后不可信 | 每一次提权都要能对应到一次登录,每一条授权变更都要有独立记录;缺任何一条时间线就断了 |
| 共享账号(反例) | 多人共用一个用户名,或共用一把私钥 | 为了省事发放统一的 deploy/ops 账号,或共用一份口令 | 审计只能到账号粒度,永远到不了人;吊销一个人的访问等于影响所有人,因此实际上没人会去吊销 | 会让另外三层全部失效:授权无法按人细分,凭据无法按人吊销,留痕无法归属到具体的人 |
这一层的操作顺序是很多人搞反的地方。正确的顺序是:先建好普通账号并确认它能登录、能提权,再禁止 root 的远程登录,最后再谈密钥。顺序反过来的代价非常具体——如果你在没有验证普通账号可用之前就把 root 登录关掉,而手头又没有控制台、带外或者救急通道,那你把自己锁在门外了,接下来要走的流程比重装还麻烦。这类事故在只有一两台机器的团队里尤其致命,因为往往根本没有第二条路。
为什么禁 root 要排在最前面?因为只要 root 还能远程登录,后面所有的设计都会被绕过去。人天然会走最短路径,只要 root 这条路开着,就会有人图省事直接用它,你精心设计的 sudo 规则、你要求的密钥、你开的日志,在 root 会话里全都可以被顺手关掉。禁止 root 远程登录不只是安全建议,它是让后面两层生效的前提条件。
具体落到配置上,是 sshd 服务配置里那个控制 root 登录方式的选项,设为不允许通过 SSH 以 root 身份认证;改完要重载服务让它生效。这里有个实操细节要说清:不要直接改完就断开当前连接,先另开一个终端窗口用普通账号完整走一遍"登录—提权—执行一条需要 root 的命令",确认通了再关 root。多花的两分钟省掉的是一个下午。
禁掉 root 之后,还有一个连带决定要做:root 的口令还留不留、su 还能不能用。如果 su 到 root 只需要一个大家都知道的根口令,那你只是把"直接以 root 登录"换成了"登录后再变成 root",归属问题一点没解决。通行做法是把提权收敛到 sudo 这一条路上,因为 sudo 天然带有"谁提的权、提权后跑了什么"的属性,而 su 只留下"某人变成了 root",之后干什么就全黑了。
接着才是认证方式的选择。口令认证的两个问题是老生常谈但依然真实的:一是面临暴力破解,只要端口暴露在公网上,持续尝试的流量就会一直存在;二是口令复用,同一个人往往在多个系统上使用相同或近似的口令,一处泄露就连带其他地方。密钥认证解决的是这两点——不需要在网络上传输可猜测的秘密,也就不存在被猜中的问题。
但密钥不是免死金牌,它的安全性完全转移到私钥本身的保护上,这里有三个变量值得逐个确认。私钥有没有设 passphrase:没有 passphrase 的私钥文件,谁拿到这个文件谁就能用,等同于把口令写在了文件里;设了 passphrase,私钥文件被拷走之后还有第二道门槛,而且给私钥被加载之前留出了一段可以补救的时间。私钥放在哪:放在个人电脑的个人目录下是一种风险等级,放在跳板机、公共服务器、共享网盘上是另一种,后者意味着私钥脱离了本人的控制范围。有没有被塞进长期运行的 agent 里:为了不用每次输 passphrase 而把私钥长期加载进认证代理,确实方便,但代价是只要能接触到这个代理的人就能用它完成认证,passphrase 的保护在加载之后就不再起作用了。
还有一个常被忽略的点:公钥灌进服务端时必须带注释。authorized_keys 文件里每一行末尾的注释字段,是几个月之后你还能认出"这把 key 是谁的"的唯一依据。没有注释的公钥行,在人员变动时就是一笔糊涂账——你看到一大串公钥,不知道该删哪一行,于是最可能的结果是干脆全留着。带注释这件事零成本,但它决定了后面"吊销"这个动作到底能不能执行。
生成一对密钥、把公钥灌进服务器,这一步几乎所有人都做得很好,因为它简单、有即时反馈。真正拉开差距的是吊销——人员离职、设备更换、怀疑私钥外泄的时候,你在服务端到底要动哪些东西。这一步没有即时反馈,做漏了没人会发现,直到某天出事。
先说一个反直觉的顺序问题:怀疑私钥外泄时,先吊销,再排查。很多人的第一反应是"我要先看看他到底干了什么",于是先去翻日志,翻几个小时。但在这几个小时里那把私钥还能正常登录,排查窗口本身就是暴露窗口。正确顺序是先让凭据失效,把门关上,再从容地查他之前做过什么。
吊销在服务端侧需要走一遍的清单,我按实际容易被漏掉的顺序列:
第一,删目标机器上的公钥行。这是最基础的一步,但要认准是删"那个人那把 key"的那一行,而不是清空整个文件。没有注释的公钥行在这里就会暴露问题——你分不清哪行是谁的,删错行等于把别人的权限也吊销了。
第二,确认公钥的存放位置不止一处。sshd 的配置里有一个指定公钥文件位置的配置项,它不一定是你以为的那个默认路径。除了用户家目录下的那个文件,还要检查服务配置里指定的其他位置、以及历史上是否有人为了图方便把公钥写到了别的地方。改过这个配置项的环境尤其要注意。
第三,检查同一个人是否在其他账号下也放了同一把公钥。这是最常漏的一条:同一个人可能在自己的账号、在运维账号、在部署账号下面都放了同一把公钥。你只删了其中一个账号下的那一行,剩下几个账号依然开着门。所以吊销动作要以"这把 key 出现在哪些账号的授权文件里"为单位来查,而不是以某个账号为单位。
第四,账号本身要不要处理,是另一件独立的事。删公钥和删账号是两个动作:删公钥是"吊销这把钥匙",锁账号(把口令失效、把登录 shell 改成一个不允许登录的程序)或者删账号是"吊销这个人"。人员离职时两个都要做;只是换设备时只做前一个就够了。
第五,检查这个人留下的定时任务。用户的定时任务、一次性任务计划、以及以该用户身份运行的常驻服务,在账号被处理之后不会自动消失。如果它们还在跑,那这个人的"影响力"就没有真正结束。这一步在做离职处理时最容易被遗忘。
第六,同步检查 sudoers 里针对这个账号的授权行。如果 sudoers 里存在按用户名写的授权规则,账号处理掉之后这些规则就成了悬空配置,既没人用也没人删,时间长了会干扰审计——你在看授权清单时会看到一堆对不上人的条目。
除了这张清单,还有一个更大的坑:镜像和备份会让删掉的公钥复活。从旧镜像克隆一台机器、从系统盘快照恢复、用自动化脚本按旧模板初始化,这三种操作都会把上一代的 authorized_keys 原样带回来。你上个月辛苦清理掉的公钥,这个月因为一次扩容又全部回来了,而且没人会告诉你。所以凭据管理到最后一定会落到"台账"这件事上——有一份能导出、能查询的记录,写清楚哪把 key 属于谁、现在应该出现在哪些机器上。台账不用复杂,一张表就够,但它必须存在,并且在每次密钥变动时同步更新。没有台账的密钥管理,本质上是在赌记性。
进入授权层,第一个要纠正的认知是:sudo 的配置不是只有"全给"和"不给"两档。它是一个有相当多可调项的策略语言,而其中真正决定安全性的是命令粒度——你能把"这个人可以执行什么"描述得多精确。
sudoers 里可调节的变量,值得逐个过一遍:
可以执行哪些命令。这是主变量。写法上可以逐个列出命令的绝对路径,也可以用命令别名把一组命令打包成一个名字,然后按角色引用这个别名。绝对路径是必须的——不写绝对路径意味着命令的实际结果取决于当时的 PATH 环境变量,等于把决定权交出去了。
命令带不带参数、参数写不写死。这是粒度最细也最容易被忽略的一层。允许"重启 nginx 这一个服务"和允许"重启任意服务",在配置文件上只差参数部分,但在实际风险上差了一个量级。后者意味着可以重启数据库、可以重启防火墙、可以重启任何东西。同理,允许"改某个特定配置文件"和允许"用编辑器改文件"完全不是一回事,下一节专门讲这个。
要不要输入密码、要不要再次验证身份。sudo 默认会要求输入当前用户的口令做一次确认,这个确认的意义不只是防外人,更是给操作者一个"我马上要提权了"的停顿点。可以配成不需要密码(NOPASSWD),这对自动化脚本是必要的,但对人的日常操作,去掉这个停顿点的代价往往被低估。也可以配成要求更严格的时间限制,让提权确认在较短时间后过期。
以谁的身份执行。sudo 默认是以 root 身份,但它可以被限定成以某个特定的服务账号身份执行。这个能力用得不多,但在"应用只需要以应用账号身份跑某些命令"的场景下非常好用——它让授权的范围从"root 的一部分"收窄成"某个非特权身份的一部分",风险面小很多。
环境变量怎么处理。sudo 默认会重置环境变量,这个行为是有明确理由的:如果不重置,被调用的程序会继承调用者的 PATH、以及某些能改变动态链接行为的变量,而这些变量可以被用来在提权执行的瞬间塞进自己的代码。除非有明确需求,否则不要为了"让命令跑起来"而放开环境传递或保留某些变量——遇到"加了 sudo 就跑不了"的情况,正确做法是查清楚是哪个变量导致的,而不是整体放开。
规则写在哪个文件里也有讲究。直接在主配置文件里堆规则,很快就变成几百行没人敢动的东西;按角色拆成 /etc/sudoers.d/ 下的多个小文件,每个文件对应一个角色,读起来清楚,回滚也容易——删掉一个文件就撤掉一个角色的授权。修改一定要用 visudo 来做,它会在保存前做语法检查,避免写出一份语法错误的配置导致 sudo 整体不可用。这一点在生产机器上不是"建议",是纪律:一个语法错误的 sudoers 会让所有人的提权同时失效,而这种失效往往发生在最需要提权的时候。
判断粒度是否合格,可以用一个标准:把你写的每一条授权规则拿出来,问一句"如果这个人想干坏事,这条规则单独能让他做到哪一步"。如果答案是"只能重启 nginx",那这条规则是合格的;如果答案是"能做 root 能做的任何事",那这条规则写得再长也没有意义。下一节讲的,就是那些看起来只开了一条命令、实际答案却是后者的情况。
这是 sudo 授权里翻车率最高的地方,也是最容易被忽视的地方,因为它在配置文件上看起来毫无问题——一行命令、一个绝对路径、没有通配符,非常有"最小权限"的样子。问题出在被授权的那个程序本身的能力上。
先说原理。一条 sudo 授权的真实范围,等于"以 root 身份运行这个程序之后,这个程序能让你做到什么",而不是"这个程序名字听起来是干什么的"。而相当多的程序,在运行过程中有能力启动另一个进程。一旦这个能力存在,你给它的授权就不是"一条命令",而是"任意命令"。
编辑器是最典型的一类。给某人授权用 root 身份运行 vim,看起来是"让他能改配置文件",非常合理的需求。但 vim 里有执行外部 shell 命令的功能,在编辑器里敲一个调用 shell 的指令,那个 shell 就是以 root 身份运行的,从此想干什么干什么。同理的还有 less、more、man 这类分页和查看工具——它们通常都有一个调用外部命令的快捷键,用法户在查看帮助、查看日志的界面里按一下就出去了。社区里有人专门整理过这类"可以被用来逃逸出完整 shell 的程序清单",名单之长会让人意外。说实话,把编辑器或分页器放进 sudo 授权清单,这类配置我一般不推荐,宁可改用"允许以 root 身份写入指定文件"的其他方式,哪怕多绕一步。
解释器是一类更直白的。python、perl、ruby、awk 这类东西,只要能以 root 身份运行,就等于有了一个 root 的交互式执行环境——因为它们本来就是用来执行任意代码的。给某人授权"用 root 跑 python 脚本",如果授权的是解释器本身而不是某个具体的脚本文件,那这条规则就是完整 root。
压缩包工具是容易被漏掉的一类。tar、zip、unzip、ar 这些工具的部分实现具备在解包过程中执行外部命令的能力,或者能以 root 身份把文件写到任意路径。而"能以 root 身份写任意路径的文件"这件事本身,就已经等价于 root 了——因为可以把自己的 sudoers 文件覆盖进去。
文件操作类命令要看目标路径。cp、tee、dd、install 这类命令,如果授权时没有限定目标路径,那么"以 root 身份复制文件到任何地方"就等于"可以改写系统上任何文件",包括 sudoers 本身、包括 SSH 的服务配置、包括任何可执行文件。判断法则很简单:只要这条命令能在 root 身份下写入任意文件或启动任意进程,它就等于 root,跟你写在 sudoers 里的那个命令名长短无关。
再说说通配符这个独立的坑。为了省事,有人会在规则里写"允许执行某个目录下的所有命令",或者"允许某命令带任意参数"。前一种等于把整个目录放进授权范围,而 /bin、/usr/bin 这类目录里能启动 shell 和能写文件的程序多得很;后一种把好不容易限定好的参数又放开回去了。更隐蔽的是中间匹配:写"允许 systemctl restart 任意对象",本意是让他重启服务,实际允许的范围包括重启任何 unit,也包括某些允许在 unit 定义里指定执行命令的用法。别被"只写了一条命令"这个表象忽悠了,要看这条命令展开之后是什么。
自查的方法有两个,都很直接。一个是站在被授权者的视角,用 sudo 的列表功能查看自己当前能执行哪些命令,把输出逐条按上面的原则过一遍;另一个是站在审计者视角,用同样的方式查看某个指定账号的授权范围,确认和他实际应该做的事一致。这个自查应该在每次改完授权规则之后立刻做一次,而不是等到季度审计。
共享账号——几个人共用一个用户名,或者共用一把私钥、一个口令——是权限治理里最省事的偷懒,也是最危险的一种。它危险的地方在于,它会让前面两层的全部工作同时失去意义。
对授权层来说:授权规则是按账号或按组写的,一个账号背后站着五个人,那么这条规则的真实作用范围就是五个人里权限最大的那个人的需求。你为了照顾其中一个人需要执行的高危命令,把这条规则开给了全部五个人。按角色分组这件事在共享账号面前彻底失效,因为"角色"这个维度被压平成了"这个账号"。
对凭据层来说:共享凭据是无法吊销的。吊销一个人的访问意味着换口令或删密钥,而这个动作会影响其余所有人,需要通知、需要重新分发、需要所有人重新配置一遍。代价高到实际上不会有人主动去做——这才是共享账号最致命的地方,它不是"吊销比较麻烦",而是"实际上永远不会被吊销"。离职同事手里的口令,往往在他离职一年后仍然有效。
对留痕层来说:这是最彻底的一击。所有动作都归属到同一个用户名,审计系统记录得再完整,也只能回答"是这个账号做的",无法回答"是人里的哪一个"。可追溯性需要的不是"有记录",而是"记录能指向一个具体的人",共享账号让这个链条在最后一环断掉。前面所有的密钥管理、sudo 粒度、日志外送,在这种情况下全部白做。
当然有例外,而且这个例外必须讲清楚,否则规则就会被误用。机器身份是可以共享的:CI 系统用的部署账号、应用用来拉代码的账号、监控用来采集指标的账号,这些账号背后是一个进程而不是一个人,多个部署任务共用同一个账号完全正常。判断标准只有一句:这个账号背后是一个人还是一个进程。是进程,就允许共用,但必须把它的授权粒度收到最紧——限定到具体命令、不要给登录 shell、只允许密钥且不允许口令认证;是人,就必须一人一号,没有例外。
现实中还有一种变体值得单独点出来:账号本身是一人一号的,但为了方便,大家互相知道彼此的口令,或者把私钥拷来拷去。这在形式上没有违反"一人一号",在实质上和共享账号没有区别——审计依然无法把动作归属到具体的人。所以"一人一号"这条规则的完整表述应该是"一人一号,且凭据不流动"。
留痕这一层最容易被做成"开了很多日志",但日志量不是目标,能回答"是谁"才是。所以先要定的不是记多少,而是记什么。至少要覆盖下面这几类动作:
登录与登出。谁、什么时候、从哪个来源地址、用的哪种认证方式、成功还是失败。失败的登录尝试尤其重要——它是唯一能在"还没出事"的时候给你信号的记录,成功的登录只是事实,失败的登录往往是意图。
提权执行。每一次 sudo 调用都要留下:执行者、时间、所在终端、以谁的身份、执行了什么命令、是否被允许、执行结果。这里有个细节:被拒绝的提权尝试比成功的更值得关注,因为它在告诉你有人在试图越权。
sudoers 文件自身的变更。这一条单独列出来是因为它属于"权限配置的变更",价值高于普通命令,下一节专门讲。
SSH 公钥的增删。往授权文件里加一行公钥,等于给某人开了一扇门。这个动作如果不留痕,那么"这个人是怎么进来的"这个问题永远答不上来。
关键服务配置的修改。Web 服务器配置、数据库配置、防火墙规则、系统服务单元文件,这几类文件的变化直接改变机器行为,是故障复盘时最先要查的东西。
用户与组的增删改。新建账号、加组、改组成员,这些动作改变的是权限结构本身,和前面几类是同一量级。
sudo 自身就带有日志能力,它的调用日志能覆盖上面第二条,配置里指定日志文件位置即可。更进一步,sudo 还支持记录会话的输入输出内容——也就是说不只记"他跑了 bash",还能记下在这个 root shell 里敲了什么。这个功能对排查很有用,但有两个前提要清楚:它只覆盖通过 sudo 进入的会话,用户没有提权时敲的命令不在其中;它会记录下命令里的所有内容,包括可能出现在命令行上的敏感参数,是否开启要自己权衡。
现在讲这一层最关键的一点:只写在本机的日志不能算数。理由很直接——拿到 root 权限的人,第一件事通常就是清理痕迹,而本机日志是他能碰到的文件。修改或删除 /var/log 下的记录、改写登录记录文件、把某个时间段抹掉,这些动作对拥有 root 权限的人来说没有技术难度。于是出现一个尴尬的循环:你最需要日志的那个场景(机器被攻陷),恰恰是本机日志最不可信的场景。
所以留痕必须外送:把日志在本地产生的同时,转发一份到本机之外、且本机权限触及不到的位置。这个要求不复杂,也不要求你建一套完整的日志分析平台——只要"另一份在别处"这件事成立,本机日志的可信度问题就解决了大半。有两个配套条件容易漏:时间要同步,多台机器、多个日志源如果时钟不一致,事后拼时间线时会拼出一堆自相矛盾的顺序;外送通道本身要可靠,通道断了要有提醒,否则"外送"只是个心理安慰。
再补一条常被忽略的反向信号:日志突然断档本身就是一条记录。如果某台机器往常每小时都有记录,突然某个时段什么都没有,那么"没有记录"这件事比任何一条具体记录都更值得查。把日志断档也当成需要告警的事件,成本很低。
这一节单独拿出来讲,是因为它是三层模型里最容易形成闭环漏洞的地方。想清楚一件事:如果有人能修改 sudoers,那么他同时也能拿到任何他想要的权限,并且——更麻烦的——他能关掉针对自己的日志。这意味着针对"权限配置变更"的留痕,其审计价值高于任何一条普通命令。
要记的东西很明确:/etc/sudoers 主文件的变化、/etc/sudoers.d/ 目录下每个文件的增删改。因为按角色拆分之后,绝大多数变更实际上发生在这个目录里,只看主文件会漏掉绝大部分。这些文件体积小、变更频率低,监控成本几乎可以忽略,但漏掉它的代价是整个授权层失去可信度。
一个非常实用的做法:把 sudoers.d 目录纳入版本管理。每次修改都是一次提交,有提交人、有时间、有 diff,谁在哪一天把哪条规则加进去的,一目了然。这个做法的额外好处是回滚也变得简单——改坏了直接回到上一个版本。配合 visudo 的语法检查,能挡住"写错导致全员无法提权"这类事故的一半。注意版本管理仓库本身的位置要在被管理的机器之外,否则等于没做——理由和上一节的"外送"完全一样。
同样的逻辑要套用到 authorized_keys 上。往授权文件里加一行公钥,是"给自己开一扇门"的动作,它和改 sudoers 属于同一性质。把家目录下的授权文件、以及 sshd 配置里指定的其他公钥位置,一并纳入变更监控,成本很低。这条和上上节的"公钥增删要留痕"是同一件事的两个侧面:那一节说要记,这一节说要记在别人改不到的地方。
还有一个收敛点常常被跳过:谁能修改 sudoers 本身。如果很多人都能编辑这个文件,那前面所有的记录都只是在描述一个权力分散的状态。通行的做法是把"修改权限配置"这件事收敛到极少数人,并且要求这个动作走一个可留痕的流程,而不是谁 root 用得顺手谁就去改一次。改权限的权限,是整台机器上最该被限制的权限。
判断这一层做得合不合格,问一句就够了:如果有人现在悄悄给自己的账号加了一条 NOPASSWD 全量授权,我多长时间能知道。如果答案是"下一季度审计的时候",那这一层是断的;如果答案是"下一次变更同步时就会出现在 diff 里",那才是成立的。
三层各自做完之后,还要做一件事:确认它们真的都对上了。因为前面讲过,这三层是串联的,而串联系统里最容易出现的情况是每一层单独看都"做了",但拼起来对不上——比如日志在记、但记的那个账号是共享的;比如密钥在管、但 sudoers 里还留着通配符。所以需要一个互验的动作。
方法很朴素:挑一次真实发生过的操作,拿它作为样本,去三个地方分别查,看能不能拼成同一条时间线。假设样本是"昨天下午有人重启了一次 nginx",那么:
凭据层查什么。查登录记录,确认那个时间段有人登录过,登录用的账号是谁,来源地址是否合理。然后去授权文件里找这把公钥的注释,确认它能对上一个具体的人。如果对不上——比如注释是空的或者写着某个已经离职的人——那凭据层的台账就是断的。
授权层查什么。用 sudo 的列表功能查看这个账号当前的授权范围,确认"重启 nginx"这条命令确实在清单里,并且清单里没有超出他职责的命令。如果实际执行的命令不在清单里,只有两种可能:规则被改过(回到上一节的问题),或者规则里存在通配符,把这条命令"意外地"覆盖了。
留痕层查什么。在 sudo 的调用日志里找这一条,确认记录了执行者、时间、命令;再去外送的那一份里找同一条,确认它也到了。两者缺一,留痕层就不完整。
把三份结果放在一条时间线上看:某时某分某账号登录 → 一分钟后该账号提权执行了某命令 → 日志里两条记录都在,且外送的那份也在。能对上,说明这次操作的三层都成立。对不上的情况各有各的含义,值得逐个记住:
有登录记录,没有提权记录。两种可能:一是这个人根本没通过 sudo 提权,而是用别的路径拿到了 root——root 远程登录没关干净、su 还开着、或者存在未纳管的后门账号;二是提权日志没有正确外送,或者记录功能没开。前者是权限漏洞,后者是留痕漏洞,都要查,但优先级完全不同。
有提权记录,但命令不在授权清单里。说明 sudoers 被改过,或者规则里有通配符把范围放大了。这是授权层失效的直接证据,而且它暴露的是"规则的可信度"问题——你以为的授权范围,和实际生效的范围不是一回事。
有操作痕迹,但对应不上任何登录记录。这是最需要警惕的一种。可能有人用了共享账号(痕迹归属不到人),也可能这条操作根本不是通过 SSH 进来的——走了控制台、走了带外管理口、或者通过某个服务本身的漏洞执行了命令。走带外或服务漏洞的路径,意味着凭据层和授权层都被绕过了,这两层再完善也没用。
这个自查多久做一次?我的建议是:每次权限相关配置变更之后立刻做一次,另外每季度随机抽一次。不要在出事之后才做第一次,因为出事之后你会发现三层全都没准备好,而那时候已经没有补救的机会了。还有一个衡量可追溯性水平的土办法:随机挑一次近期的线上变更,从结果反推到人,看要花几分钟。这个耗时就是你的真实水平——能三分钟查到,说明三层是通的;查了一下午还没结论,说明至少断了一层。
最小权限原则说起来简单,真做下去会遇到一个硬约束:权限拆得越细,日常运维的效率越低。按人授权的话,规则数量是人数乘以机器数,任何一次职责调整都要改一大批配置,改的过程本身还容易出错。到某个规模,这套东西会因为维护成本过高而被放弃,然后退回到"干脆都给 root"。所以现实里通行的做法不是按人授权,而是按角色分组。
按角色分组的意思是:先定义几个角色,每个角色对应一组"这批人需要执行的命令集合",人通过加入或退出相应的组来获得或失去这些能力。系统层面用 unix group 承载,sudoers 里按组来写授权规则。这样规则数量变成角色数乘以机器数,而角色数是稳定的;人员变动时只动组成员,不动授权规则,改动小、可回滚、不容易出错。
角色怎么划,依据不是职级,也不是部门,而是"这批人实际需要执行的命令集合"。说得再具体一点:去看过去一个月这些人实际敲过的提权命令,按集合聚类,簇与簇之间的边界就是角色边界。凭想象划分出来的角色,上线两周就会被例外请求打穿。
几个常见角色的划分思路:应用运维需要重启和查看应用相关的服务、看应用日志、改应用自己的配置文件;数据库角色需要执行数据库相关的管理命令、看数据库日志;只读角色只需要看日志和看状态;系统管理角色才涉及用户管理、软件包、网络配置。这里最容易被忽略的是只读角色——需要看日志的人,往往因为"给他 root 最省事"而拿到了完整 root,其实他需要的能力完全可以被限定在只读范围内。把只读角色单独拆出来,是投入产出比最高的一次权限收紧。
再讲应急通道。无论权限收得多紧,都必须保留一条在紧急情况下能拿到完整权限的路,否则某天深夜需要救火时会发现所有人都不够权限,最后的结果一定是有人临时把权限放开然后忘了收回。所以关键不是"要不要留应急通道",而是"这条通道能不能被审计"。常见的做法有几种:通过带外控制台或物理控制台直接进入,这条路不走 SSH 也不走 sudo,本身就独立于三层模型之外,但它的使用通常会在带外层面留下记录;一次性提权账号,口令存放在密码管理器里,用一次改一次,使用行为触发告警;需要双人参与才能取得的临时授权。三种方式的共同点是:应急通道的日常使用成本高、且每一次使用都留下痕迹。
应急通道最忌讳的一件事是让日常账号兼任。如果某人的日常账号本身就能拿到 root,那"应急"和"日常"就没有区别,通道的存在也就失去了意义——你分不清某次 root 操作是日常运维还是应急救火。独立的应急账号还有一个附加好处:审计的时候只需要单独查这一个账号的使用记录,范围小、结论清晰。
折中的分寸也要讲清楚。不要为了追求百分之百的最小权限,把日常效率打死。举个具体的例子:重启某个服务是高频操作,把它限定成"只能重启这几个指定服务"是有意义的收紧;但如果要求每次重启都走审批,那就是把效率换成了形式,两周后一定会出现绕过。权限设计的目标是把"不该发生的操作"挡住,而不是把"经常发生的操作"变麻烦。判断标准很实在:一次收紧,如果让正常的运维动作每周多花十分钟,可以接受;如果让它每周多花两小时,那这条规则迟早会被绕过。
到这里为止讲的都是本机这一层能做的事,几个人、几台机器的规模,这套东西足够用,而且比上一套集中式系统更清楚——因为你手里的台账就是全部事实,没有抽象层。但它有一个明确的规模上限,越过了就该换方案。触发点有三个,逐个说。
第一个触发点:一次人员变动需要在多少台机器上重复操作。如果每次有人离职、有人换设备,你都要在十几台甚至几十台机器上重复一遍删公钥、改 sudoers 的操作,那么这件事的出错概率会随机器数线性上升——漏一台就是留一扇门。更实际的问题是:你还能不能准确回答"这个人现在还能进哪几台机器"。当你对这个问题没法在五分钟内给出确定答案时,台账已经跟不上现实了,这就是该换方案的信号。
第二个触发点:sudoers 里出现了大量按机器的例外。按角色分组的模型成立的前提是"同一角色在不同机器上的需求是一样的"。如果你发现规则里堆满了"某人在某台机器上额外需要某条命令"的例外条目,说明角色模型已经失效,你在用一堆补丁维持一个不再成立的抽象。继续打补丁的成本会越来越高,而收益是零。
第三个触发点:审计时需要在多台机器上拼时间线,而且拼不出来。单机三层互验是本文第十节讲的那套动作,几台机器还能手工做;当排查一个问题需要跨十几台机器对齐时间、对齐账号、对齐授权范围时,手工拼接本身就成为最大的成本。这时候缺的不是某台机器的配置,而是"跨机一致"这件事本身。
到了这个规模,该引入的是集中式方案,而集中式方案其实有两条不同的路,容易混为一谈:配置管理解决的是"分发一致性"——公钥清单、sudoers 规则由一份源生成,自动下发到所有机器,保证每台机器上的状态是同一份定义;堡垒机解决的是"身份收敛与会话审计"——所有人都从同一个入口进入,身份在这一层收敛,会话在这一层记录。两者解决的是不同问题,可以只上其中一个,也可以都上,选型不是本文要谈的事。但要提醒一句:工具不会替你设计授权粒度。角色怎么划、每条规则开到什么粒度、应急通道怎么留,这些判断在上集中式方案之前和之后都需要你自己想清楚。把一堆没有设计过的授权规则搬进集中式系统,只会让混乱变得更快、覆盖更广。
所以租机器的时候,我一般会先跟深耕 IDC 19 年(成立于 2007 年)的一万网络确认交付时的初始账号状态——是不是默认就给了 root、密钥能不能自己灌、重装之后这些会不会被抹掉;7×24 中文工单平均 5 分钟响应,这类交付细节问起来不费劲;具体初始账号策略以官网实时信息为准。这一步看着琐碎,其实决定了你后面所有权限工作的起点:如果交付时就是一个共用的 root 口令,那前面讲的凭据层要从零开始搭;如果重装会把你辛苦配好的公钥和 sudoers 一起抹掉,那么"重装"这个动作就必须被纳入你的变更流程里,否则每次重装都是一次权限模型的回退。
禁掉 root 远程登录是不是就安全了?不是。它解决的是"所有人都以同一个身份进来"这个归属问题,但登录之后还是要用 sudo 提权,而提权开多大完全是另一件事。把 root 登录关掉、然后给所有人配一条全量 sudo,等于什么都没做——只是把"所有人都是 root"换成了"所有人都能一步变成 root"。
给 NOPASSWD 是不是方便很多?方便是真的,代价也是真的。去掉口令确认之后,"经过思考才提权"这个停顿点没有了,误操作率会上升;更重要的是,脚本的自动执行和人的手动操作会混在同一类记录里,事后很难区分哪次是人敲的、哪次是任务跑的。对人保留口令确认,只对确实需要自动化的路径单独开 NOPASSWD,并且把它限定在具体命令上,这两件事不冲突。
密钥设了 passphrase 是不是就够了?passphrase 保护的是"私钥文件被别人拿到"那一刻。它保护不了私钥已经被加载进认证代理之后的使用,也保护不了私钥被拷到一台共享机器上之后的使用。所以 passphrase 是必要条件,不是充分条件,私钥的存放位置同样重要。
日志开得越全越好吗?不是。日志量的目标和可追溯性无关,能回答"是谁"才是目标。把注意力放在本文第八节那六类动作上,比全开更有用;另外一定要记得失败的登录和失败的提权尝试比成功的更值得关注——成功的记录是事实,失败的记录是信号。
我们只有三台机器、四个人,也要搞这么复杂吗?不用全搞,但有四件事一天之内就能做完,而且应该立刻做:关掉 root 远程登录、一人一号且凭据不共用、按角色写 sudo 规则而不是全给、日志往本机之外送一份。这四件事对应三层模型的最小可用版本,做完之后这台机器的权限治理就从"断链"变成"成立"。剩下的台账、版本管理、应急通道,可以慢慢补。
密钥和口令到底哪个安全?密钥,但前提是你能管得住吊销。一把从来没人管过的密钥,和写在便签上的口令没有本质区别——区别在于人们普遍对前者更有安全感,而这种安全感往往是没有依据的。
本文内容基于公开的 Linux 系统管理通用技术知识整理而成,涉及 SSH 服务配置、sudo 授权机制与系统日志记录能力,均为操作系统自带功能的通用用法,不针对任何特定发行版或特定版本,实际配置路径与可用选项请以所用系统的官方文档为准。文中提到的"典型场景""假设场景"用于说明思路,并非特指某一真实客户案例。本文为行业技术分析,涉及产品、配置与价格以签约时最新报价与合同为准,参考来源 https://www.idc10000.net/ 相关页面。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品