1. 这不是配置文件,是Linux系统的“户籍档案”——从真实运维现场讲透/etc/passwd和/etc/group
你刚接手一台生产环境的CentOS服务器,凌晨三点收到告警:某个定时任务突然失败,日志里只有一行冰冷的报错:Permission denied: /var/log/app/backup.log。你第一反应是查权限,ls -l /var/log/app/显示属主是appuser,但id appuser却返回no such user。你心里一沉——用户没了?还是被误删了?这时候,你不会去翻手册,而是直接敲出cat /etc/passwd | grep appuser。三秒后,屏幕上空空如也。问题定位完成:这个用户记录确实被清掉了,不是权限问题,是身份认证层就断了。这就是/etc/passwd的真实分量——它不是一份可有可无的配置清单,而是整个Linux系统识别“你是谁”的唯一法定依据,是所有权限校验、进程归属、日志归因的起点。同理,当你在Docker容器里执行groupadd -g 1001 devteam后,发现宿主机上getent group devteam查不到,而容器内却能查到,这背后就是/etc/group文件在不同命名空间中的隔离与映射逻辑。本文不讲教科书定义,只还原我过去十年在金融、电商、云厂商一线踩过的坑、调过的参、救过的火。你会看到:为什么root:x:0:0:root:/root:/bin/bash里那个看似多余的x实际上是安全防线的第一道闸机;为什么nobody:x:99:99:Nobody:/:/sbin/nologin这个“幽灵用户”在NFS挂载和Web服务中承担着不可替代的降权角色;为什么修改/etc/group后,已登录用户的组成员身份不会实时刷新,必须重新登录或newgrp才生效——这不是Bug,是POSIX标准对会话上下文一致性的刚性要求。全文所有命令、字段、案例均来自真实生产环境,你可以直接复制粘贴验证,也可以把它当作一张随身携带的排障地图。无论你是刚装完Ubuntu的新手,还是正在为K8s集群中ServiceAccount与Linux UID映射发愁的SRE,只要你的工作涉及用户、权限、服务部署,这篇内容就不是“可读”,而是“必读”。
2./etc/passwd深度解剖:7个字段,每个都是一个决策点
2.1 字段结构与语义:从“冒号分隔符”说起
/etc/passwd是一个纯文本文件,每行代表一个用户,字段间用英文冒号:分隔,共7个字段。它的格式是 POSIX 标准强制规定的,任何试图用空格、制表符或逗号替代冒号的操作都会导致getpwent()等C库函数解析失败,进而引发su、login、sshd等核心服务崩溃。这不是语法洁癖,而是底层ABI契约。我们以最典型的daemon:x:2:2:daemon:/sbin:/sbin/nologin为例,逐字段拆解其设计逻辑:
第1字段:用户名(Login Name)
这是用户登录时输入的标识符,也是ls -l输出中OWNER列显示的内容。它必须全局唯一,且不能包含冒号、换行符或控制字符。很多人误以为用户名长度无限制,实则受NAME_MAX系统常量约束(通常为255字节)。我在某次迁移旧系统时,将一个含中文名的LDAP用户同步到本地/etc/passwd,用户名写成张三_2023,结果usermod -aG wheel 张三_2023报错invalid user name。排查发现是终端编码问题导致_被解析为全角字符,实际字节数超限。解决方案不是改名,而是统一使用iconv -f utf8 -t ascii//TRANSLIT预处理用户名。
第2字段:密码占位符(Password Field)
这里永远是x,这是现代Linux发行版的统一约定。它本身不存储密码,而是一个指向/etc/shadow的指针。这个设计源于1990年代的安全演进:早期Unix将加密后的密码明文存于此处,任何能读取/etc/passwd的用户(比如通过FTP下载)都能拿到密文进行离线爆破。将密码移至仅root可读的/etc/shadow,是权限最小化原则的第一次大规模落地。x不是随意选的字符,它是crypt(3)函数输出的Base64编码字符集中的一个合法起始符,确保向后兼容。如果你在/etc/passwd中看到*或!,说明该用户被锁定(usermod -L),此时即使知道密码也无法登录,因为PAM模块会直接拒绝认证。
第3字段:用户ID(UID)
这是内核真正识别用户的身份ID,一个32位无符号整数。0是root的特权UID,内核硬编码此值为最高权限。1-999是系统保留UID,分配给daemon、syslog、mysql等服务账户,它们通常没有交互式shell。1000+是普通用户起始UID,由发行版安装程序或useradd命令自动分配。关键点在于:UID是内核调度和权限检查的原子单位,用户名只是给人看的别名。我曾遇到一个诡异问题:某Java应用日志显示java.lang.UnixProcess.forkAndExec失败,错误码EACCES。strace追踪发现,它尝试以UID1001创建子进程,但该UID在/etc/passwd中对应用户已被userdel -r彻底删除,只剩/etc/shadow中残留记录。内核允许进程以任意UID运行(setuid(2)),但glibc的getpwuid()在构建环境变量USER时会调用getpwent()查询/etc/passwd,查不到就返回空,导致Java的System.getProperty("user.name")为null,触发后续空指针异常。根本解法不是恢复用户,而是让应用不依赖USER环境变量,改用getuid()系统调用。
第4字段:主组ID(GID)
它指定用户登录时的默认组(Primary Group),即ls -l中GROUP列显示的组。这个GID必须存在于/etc/group中,否则用户无法完成登录。有趣的是,它与用户权限无直接关系——文件权限检查时,内核只认UID和附加组(Supplementary Groups),主组仅用于新创建文件的默认属组。touch test.txt生成的文件属组就是此GID。这也是为什么usermod -g newgroup username会改变新文件的默认属组,但不影响已有文件权限。
第5字段:GECOS信息(User Information)
这是一个用逗号分隔的复合字段,传统上包含Full Name,Room Number,Work Phone,Home Phone,Other。现代系统大多只填第一个字段(全名),其余留空。它的存在价值在于:finger命令、邮件系统、LDAP同步工具都依赖此字段做用户信息展示。我在某银行核心系统审计中发现,所有生产账号的GECOS字段均为Audit User,而开发测试账号则填有真实姓名和工号。这成为自动化脚本识别账号类型的关键特征,比单纯查UID范围更可靠。
第6字段:家目录(Home Directory)
这是用户登录后的初始工作目录,cd命令不带参数时即跳转至此。它必须是绝对路径,且对用户有读、执行权限(r-x),否则shell无法进入。一个经典陷阱是:useradd -m -d /home/newuser newuser创建用户后,/home/newuser目录权限为700,但若管理员手动chmod 755 /home/newuser,则其他用户可遍历其目录结构,泄露.bash_history等敏感文件。正确做法是保持700,并通过setfacl给特定组添加必要访问权限。
第7字段:登录Shell(Login Shell)
指定用户登录后启动的程序。/bin/bash是交互式shell,/sbin/nologin和/bin/false是禁用登录的“黑洞shell”。二者区别在于:/sbin/nologin会向用户显示一条友好提示(如This account is currently not available.),而/bin/false直接退出,返回错误码1。在高安全等级系统中,我倾向用/bin/false,因为它不产生任何输出,减少攻击面。某次渗透测试中,红队利用nologin的提示信息确认了服务账户的存在,进而针对性爆破SSH密钥。
2.2 安全红线:哪些操作会直接导致系统瘫痪?
修改/etc/passwd是高危操作,以下行为在生产环境等同于“拔网线”:
提示:绝对禁止直接用
vi /etc/passwd编辑!必须使用vipw命令。vipw会自动加锁/etc/passwd和/etc/shadow,防止并发编辑导致文件损坏。我曾见同事在双人协作时,一人用vi,一人用usermod,结果/etc/passwd最后一行被截断,root用户记录丢失,系统彻底无法登录,只能进单用户模式修复。
注意:不要手动修改root用户的UID/GID。内核和大量系统工具(如
sudo、cron)硬编码了UID 0的特权逻辑。将root UID改为1,会导致su -失败,systemctl无法管理服务,甚至ls的-l选项显示UID=1而非root,造成巨大认知混乱。
警告:避免在用户名中使用特殊字符。
useradd "test-user"会创建名为test-user的用户,但某些老旧的备份脚本(如用awk -F: '{print $1}'解析)会把连字符当作字段分隔符,误判为两个字段,导致脚本崩溃。生产环境应严格遵循[a-z][a-z0-9_-]*正则规范。
2.3 实战技巧:如何从/etc/passwd快速获取关键情报?
与其逐行grep,不如用结构化命令提取信息。以下是我在巡检脚本中高频使用的组合技:
# 1. 找出所有UID大于1000的普通用户(排除系统账户) awk -F: '$3 > 1000 {print $1,$3,$6,$7}' /etc/passwd | column -t # 2. 检查是否存在无家目录或家目录不存在的用户(安全隐患) awk -F: '$6 != "/" && !($6 ~ /^\/home\//) && !(-d $6) {print $1,"家目录不存在:",$6}' /etc/passwd # 3. 列出所有使用 /sbin/nologin 或 /bin/false 的服务账户,并按GECOS分组统计 awk -F: '$7 ~ /nologin|false/ {gsub(/,.*/, "", $5); count[$5]++} END {for (i in count) print i ": " count[i]}' /etc/passwd | sort -k2nr这些命令的核心思想是:把/etc/passwd当作一个数据库表,用awk做SQL式的查询。column -t让输出对齐,sort -k2nr按第二列数值倒序排列,都是提升可读性的细节。记住,awk的$0是整行,$1到$7是各字段,-F:指定分隔符,-d $6是shell测试目录存在性,这些是Linux文本处理的基石能力。
3./etc/group全景透视:组不是“权限集合”,而是“访问令牌分发中心”
3.1 字段结构与权限模型的本质差异
/etc/group同样是冒号分隔的纯文本文件,但只有4个字段:groupname:password:GID:userlist。它与/etc/passwd的最大区别在于:组本身不拥有权限,它只是将一组用户ID(UID)打包,供文件系统在权限检查时快速匹配。当一个进程访问文件时,内核检查的不是“这个组有没有权限”,而是“当前进程的UID是否等于文件属主,或者当前进程的GID列表中是否包含文件属组”。因此,组的核心价值在于“批量授权”和“权限继承”。
我们以docker:x:999:alice,bob,carol为例:
第1字段:组名(Group Name)
必须唯一,且不能与用户名冲突(POSIX要求)。docker组名是Docker官方约定,将用户加入此组,即可免sudo执行docker命令。原理是:/usr/bin/docker的属组为docker,且设置了setgid位(-rwxr-sr-x),当alice执行时,进程的有效GID变为999,从而获得对Docker守护进程socket文件/var/run/docker.sock的读写权限。第2字段:组密码(Group Password)
现代系统中几乎总是x,表示密码存储在/etc/gshadow。gpasswd命令管理此密码,用于newgrp命令切换主组。普通用户很少用到,因为usermod -aG添加的是附加组,无需密码。第3字段:组ID(GID)
与UID类似,0是root组,1-999是系统组。关键点在于:一个用户可以同时属于多个组(附加组),但只能有一个主组(Primary Group)。主组由/etc/passwd的第4字段决定,附加组由/etc/group的第4字段决定。id username命令输出的groups=列表,第一个是主组,后面是附加组。第4字段:用户列表(User List)
以逗号分隔的用户名列表,表示这些用户是该组的成员。注意:此字段不包含主组用户!这是初学者最大误区。例如,alice的主组是alice(由useradd自动创建),那么/etc/group中alice:x:1001:这一行的用户列表为空。只有当usermod -aG docker alice后,docker行的用户列表才追加alice。这意味着:/etc/group只记录“额外加入的组”,主组关系隐含在/etc/passwd中。
3.2 组权限的“延迟生效”机制与会话生命周期
这是/etc/group最易被误解的特性:修改组成员关系后,已登录用户的组列表不会立即更新。原因在于:Linux进程的组ID列表(/proc/[pid]/status中的Groups:行)是在用户登录时由login或sshd进程一次性从/etc/passwd和/etc/group读取并固化到进程的cred结构体中的。后续对/etc/group的修改,只影响新登录的会话。
我曾在线上K8s集群升级时遭遇此问题:为适配新版本,需将所有运维用户加入k8s-admin组。执行usermod -aG k8s-admin alice后,id alice仍不显示k8s-admin,kubectl get nodes报错Forbidden。解决方法只有两个:
alice退出所有SSH会话,重新登录;- 在当前会话中执行
newgrp k8s-admin,它会启动一个新shell,其cred结构体重新读取组信息。
提示:
newgrp的本质是exec setgid(k8s-admin) /bin/bash,它会改变当前shell的有效GID,但不会影响父进程。因此,在脚本中慎用newgrp,它会导致脚本后续命令在新shell中执行,退出后父shell环境不变。
3.3 生产环境组策略设计:从“最小权限”到“职责分离”
在金融级系统中,我推行“三层组模型”:
| 组名 | GID | 用途 | 成员管理方式 |
|---|---|---|---|
sysop | 1000 | 系统级操作(reboot,mount) | 仅SRE核心成员,sudoers中白名单授权 |
appadmin | 1001 | 应用部署与维护(systemctl restart app,tail -f /var/log/app/*.log) | 各业务线负责人,通过usermod -aG appadmin动态添加 |
readonly | 1002 | 只读审计(ps aux,df -h,journalctl --no-pager -u nginx) | 所有开发、测试、QA,自动同步LDAP组 |
这种设计将权限粒度从“用户”下沉到“组”,再通过sudoers文件精细控制组能执行的命令。例如,%appadmin ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx, /bin/tail -f /var/log/nginx/*。这样,即使某个appadmin用户的密码泄露,攻击者也只能重启Nginx或查看日志,无法执行rm -rf /。
4. 用户与组的协同作战:权限检查的完整链路还原
4.1 一次文件访问的内核之旅:从open()到权限放行
理解/etc/passwd和/etc/group如何协同工作,最好的方式是追踪一个真实场景。假设用户alice(UID=1001,主组GID=1001,附加组GID=999,1002)执行cp /tmp/data.txt /var/www/html/index.html。
进程创建:
bash调用fork()创建子进程,execve("/bin/cp", ...)加载cp程序。此时,子进程的cred结构体中uid=1001,gid=1001,groups=[1001,999,1002]。源文件检查(
/tmp/data.txt):cp调用open("/tmp/data.txt", O_RDONLY)。内核检查:- 文件属主UID是否等于进程有效UID(1001 == 1001)?是,放行读取。
- (若不相等)检查文件属组GID是否在进程组列表中?否,跳过。
- (若不相等)检查其他用户(Others)是否有读权限?取决于文件mode。
目标文件检查(
/var/www/html/index.html):cp调用open("/var/www/html/index.html", O_WRONLY|O_CREAT, 0644)。内核检查:- 文件属主UID是否等于进程有效UID?
/var/www/html目录属主通常是root,1001 ≠ 0,不满足。 - 文件属组GID是否在进程组列表中?
/var/www/html目录属组通常是www-data(GID=33),33 不在[1001,999,1002]中,不满足。 - 其他用户(Others)是否有写权限?
/var/www/html目录mode通常是drwxr-xr-x,others只有r-x,无写权限,open()返回-EACCES。
- 文件属主UID是否等于进程有效UID?
此时,cp命令报错Permission denied。解决方案不是给others加写权限(极不安全),而是将alice加入www-data组:usermod -aG www-data alice,然后重新登录。这样,进程组列表变为[1001,999,1002,33],第二步检查通过。
4.2umask:隐藏在用户背后的“权限雕刻师”
umask不是/etc/passwd或/etc/group的一部分,但它与用户身份深度绑定。umask是一个掩码,用于从文件默认权限中“减去”相应位。touch创建文件的默认权限是666(rw-rw-rw-),目录是777(rwxrwxrwx)。umask 002表示:对属主、属组、其他用户,分别屏蔽---、--w、--w位,最终文件权限为664(rw-rw-r--),目录为775(rwxrwxr-x)。
umask的值由用户shell初始化脚本(如/etc/profile,~/.bashrc)设置,但它的效果直接受/etc/passwd中用户主组影响。例如,alice主组是alice(GID=1001),umask 002下创建的文件属组是alice,组内其他用户(同属alice组)可写。但如果alice被加入www-data组作为附加组,umask不会自动让新文件属组变为www-data——文件属组永远是主组,除非显式用chgrp www-data filename修改。
4.3 实操:构建一个安全的Web开发环境
以搭建一个多人协作的PHP开发环境为例,演示/etc/passwd和/etc/group的协同配置:
# 1. 创建专用系统组和用户 groupadd -g 2001 webdev useradd -u 2001 -g webdev -d /var/www/dev -s /bin/bash -c "Web Development Team" webdev # 此时 /etc/passwd: webdev:x:2001:2001:Web Development Team:/var/www/dev:/bin/bash # 此时 /etc/group: webdev:x:2001: 和 webdev:x:2001:webdev # 2. 创建项目组,让开发者加入 groupadd -g 2002 php-project usermod -aG php-project alice usermod -aG php-project bob # 3. 设置项目目录权限(关键!) mkdir -p /var/www/php-project chown webdev:php-project /var/www/php-project chmod 2775 /var/www/php-project # 2=setgid, 7=rwx for owner, 7=rwx for group, 5=rx for others # setgid位确保在此目录下创建的文件,属组自动继承为 php-project,而非创建者主组 # 4. 验证 # alice 登录后,执行 touch /var/www/php-project/test.php # ls -l /var/www/php-project/test.php 显示 -rw-rw-r-- 1 alice php-project ... # bob 可以编辑此文件,因为同属 php-project 组,且目录有组写权限这个方案中,webdev用户是服务账户,运行Web服务器(如Apache),php-project组是开发者的协作组。setgid目录和umask 002的组合,确保了“创建即共享”的协作体验,而无需频繁chgrp。
5. 排查实战:那些年我们一起修过的/etc/passwd和/etc/group故障
5.1 故障现象与根因分析速查表
| 现象 | 可能根因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
su - username报错Authentication failure,但密码正确 | /etc/passwd第2字段不是x,或/etc/shadow权限错误(非600) | getent passwd username查看第2字段;ls -l /etc/shadow | usermod -p '*' username重置密码占位符;chmod 600 /etc/shadow |
id username显示组列表为空,但usermod -aG groupname username已执行 | 用户未重新登录,或/etc/group中该组的用户列表未更新(逗号后有多余空格) | getent group groupname;grep groupname /etc/group | 退出重登;用vigr编辑/etc/group,删除多余空格 |
新建用户useradd -m newuser后,su - newuser报错No directory, logging in with HOME=/ | /etc/passwd中该用户第6字段(家目录)为空或路径不存在 | getent passwd newuser;ls -ld /home/newuser | usermod -d /home/newuser -m newuser;mkdir -p /home/newuser && chown newuser:newuser /home/newuser |
sudo命令提示user is not in the sudoers file | 用户未加入sudo组(Ubuntu)或wheel组(CentOS/RHEL) | getent group sudo或getent group wheel | usermod -aG sudo newuser(Ubuntu);usermod -aG wheel newuser(CentOS) |
ssh登录后立即退出,无错误提示 | /etc/passwd第7字段(shell)指向不存在的路径,或shell程序无执行权限 | getent passwd $USER;ls -l $(getent passwd $USER | cut -d: -f7) | chsh -s /bin/bash $USER;chmod +x /bin/bash |
5.2 真实案例复盘:一次因nologin引发的CI/CD流水线中断
背景:某电商平台的GitLab CI Runner 使用gitlab-runner用户执行构建任务。某天所有流水线卡在Preparing environment阶段,日志显示ERROR: Job failed: prepare environment: exit code 1。
排查过程:
- 登录Runner服务器,
ps aux \| grep gitlab-runner发现进程属主是gitlab-runner,但id gitlab-runner显示gitlab-runner:x:998:998::/home/gitlab-runner:/bin/bash—— shell是/bin/bash,正常。 - 尝试
sudo -u gitlab-runner bash,成功进入shell,说明用户未被锁定。 - 检查
/var/log/gitlab-runner/current,发现关键错误:fatal: unable to run hook pre-receive: Permission denied。 - 追踪到GitLab的hooks目录
/opt/gitlab/embedded/service/gitlab-shell/hooks/,ls -l显示属主是git,属组是git,权限drwxr-x---。 id gitlab-runner再次执行,发现Groups: 998 997,其中997是git组!但getent group git输出git:x:997:,用户列表为空。- 原因揭晓:
gitlab-runner用户是通过useradd -r -g git gitlab-runner创建的,-g git指定了主组为git,但/etc/group中git行的用户列表并未包含gitlab-runner(因为主组不记录在/etc/group用户列表中)。而GitLab的hook脚本依赖组权限,需要gitlab-runner在git组的用户列表中才能访问。
解决方案:
# 将 gitlab-runner 显式加入 git 组的用户列表 usermod -aG git gitlab-runner # 重启服务 systemctl restart gitlab-runner教训:-g参数只设置主组,对权限模型有特定影响的场景(如Git hooks),必须确保用户也在对应组的/etc/group用户列表中,即同时是主组和附加组成员。这违背直觉,却是生产环境的真实约束。
5.3 高级技巧:用getent统一查询,绕过文件解析陷阱
getent命令是查询用户和组信息的黄金标准,它不直接读取/etc/passwd或/etc/group,而是调用getpwnam(3)和getgrnam(3)等C库函数,这些函数会按/etc/nsswitch.conf配置,依次查询本地文件、LDAP、NIS等后端。这意味着:
getent passwd username比grep username /etc/passwd更可靠,因为它能查到LDAP用户。getent group groupname能正确解析包含空格或特殊字符的组名(/etc/group中不允许,但LDAP可以)。getent passwd(无参数)会列出所有可用用户,包括系统用户和网络用户,是全面审计的起点。
在编写自动化脚本时,永远优先使用getent,而不是直接解析文本文件。例如,检查用户是否存在,应该用:
if getent passwd "$username" >/dev/null 2>&1; then echo "User exists" else echo "User does not exist" fi而不是grep "^$username:" /etc/passwd,后者在用户名含正则元字符(如*、.)时会误匹配。
6. 进阶思考:当/etc/passwd和/etc/group遇上容器与云原生
6.1 容器镜像中的用户管理:从root到non-root的范式转移
Docker官方最佳实践强烈建议:永远不要以 root 用户运行容器进程。这直接挑战了传统/etc/passwd的使用方式。在基础镜像(如debian:slim)中,/etc/passwd包含完整的用户列表,但生产镜像应精简:
# BAD: 默认以 root 运行 FROM debian:slim COPY app /app CMD ["/app/server"] # GOOD: 创建非特权用户 FROM debian:slim RUN groupadd -g 1001 -r appgroup && useradd -r -u 1001 -g appgroup appuser COPY --chown=appuser:appgroup app /app USER appuser CMD ["/app/server"]构建后,docker run -it image cat /etc/passwd只会看到appuser和root两行。--chown确保/app目录属主为appuser,USER指令设置容器默认UID/GID。此时,/etc/passwd不再是系统“户籍”,而是一个最小化的、服务于单一进程的“身份声明”。
6.2 Kubernetes中的ServiceAccount与Linux UID映射
在K8s中,Pod的securityContext.runAsUser字段直接映射到容器内的UID。runAsUser: 1001会覆盖镜像中USER指令的设置。但/etc/passwd中必须存在UID 1001的记录,否则ps、top等命令无法解析进程属主,显示为数字UID。因此,生产镜像应在Dockerfile中预创建该用户:
ARG USER_ID=1001 RUN groupadd -g ${USER_ID} appgroup && \ useradd -r -u ${USER_ID} -g appgroup appuser这样,runAsUser和/etc/passwd保持一致,监控和日志才具备可读性。
6.3 云环境下的集中式用户管理:LDAP与SSSD的协同
在百台以上服务器的环境中,手工维护/etc/passwd和/etc/group是灾难。我们采用sssd(System Security Services Daemon)连接企业LDAP:
- LDAP服务器存储所有用户和组的完整信息(包括密码、邮箱、电话等)。
sssd作为本地代理,缓存LDAP数据,并提供nss(Name Service Switch)和pam(Pluggable Authentication Modules)接口。/etc/nsswitch.conf中配置passwd: files sss,表示先查本地/etc/passwd,再查sssd。sssd会动态生成/var/lib/sss/mc/passwd等内存缓存文件,getent命令通过nss机制透明访问。
此时,/etc/passwd退化为“本地特权账户白名单”(root,admin),所有业务用户由LDAP统一管理。useradd等命令不再适用,全部通过LDAP Admin UI或ldapmodify工具操作。这是规模化运维的必然选择。
我在某券商的混合云架构中实施此方案:IDC内网服务器直连LDAP,公有云ECS通过sssd的adprovider 连接Azure AD。getent passwd在两类服务器上返回完全一致的用户列表,实现了“一套身份,全域通行”。
7. 我的个人经验总结:关于用户和组,这三条铁律必须刻进DNA
在写下这篇文章前,我翻出了过去十年的工作笔记,那些深夜的告警、客户的质疑、审计老师的拷问,最终凝结成三条朴素的信条:
第一,永远相信getent,永远怀疑cat。cat /etc/passwd看到的只是静态快照,而getent passwd调用