YAOTU INSIGHTS

Linux /etc/passwd 字段详解与账号管理实战

Linux /etc/passwd 字段详解与账号管理实战
在我待过的几个运维群里每隔一段时间就会有人贴出一张截图sudo突然报错说当前用户不存在于密码数据库中或者某个服务起不来、日志里写着找不到指定账号。顺着线索查下去八成会落到同一个文件上——/etc/passwd。这个文件在 Linux 系统里活得非常低调权限是所有人可读体积小得可怜几十行到几百行而已很多人装了几年系统都没正眼瞧过它。可它偏偏是账号体系的户口本谁是谁、谁是超级用户、谁能登录、登录后落到哪个目录、用哪个 shell全在这一行行的冒号分隔文本里写着。这篇内容就围绕Linux /etc/passwd这一个文件展开把它拆到字段级别再把查看、解读、新增、修改、校验这条路走一遍。不管你是刚在虚拟机里装完系统、连useradd和adduser区别都还没分清的新手还是天天跟服务器打交道的运维这里面的坑大概率你都踩过或者即将踩到。1. /etc/passwd 到底是个什么东西打开这个文件之前有个反常识的事实得先说清楚/etc/passwd里面并没有密码。这个名字是四十多年前留下来的历史包袱早期 Unix 确实把加密后的口令哈希直接写在第二个字段里后来因为安全审计的需要哈希被搬去了/etc/shadow文件名却一直没改。所以第一次cat出来看到满屏的x不用怀疑自己是不是把文件改坏了那是正常状态。1.1 文件名里的历史误会与它现在的定位passwd这个词在英文里既是密码也是密码文件早期它名副其实。当年系统管理员把哈希写在所有人可读的文件里任何登录上来的普通用户都能把整份哈希拷贝走慢慢离线破解这在多用户主机时代是个巨大的隐患。于是系统把哈希迁移到了只有 root 能读的/etc/shadow原文件第二个字段统一用x占位表示真正的密码在别处查。文件名没改的原因很简单那个年代已经有大量程序、脚本、文档依赖这个路径改名带来的兼容成本远大于收益索性就一直留着。现在它的定位很清晰一份账号索引负责把用户名映射到一个数字 UID并附带这个账号的基本登录环境信息。系统内部几乎所有的权限判断都以 UID 这个数字为准而不是以用户名。你在ls -l里看到的文件属主名字是显示工具反向查这张表翻译出来的ps、top、sudo、ssh、psql、docker ps这些命令想把 UID 变成人能看懂的名字也都要走这张表。我见过有人在容器里手动改了文件属主结果ls -l里那行显示成一串裸数字第一反应是文件系统损坏其实只是容器内的/etc/passwd里没有对应的 UID 条目翻译不出来而已。理解这一点后面很多诡异现象都能自己解释通。1.2 它在登录链路里被谁读取一次典型的 SSH 登录背后会依次经过几个环节sshd接到连接请求先从/etc/passwd里查这个用户名对应的 UID、家目录和登录 shell然后交给 PAM 做认证PAM 的pam_unix模块去/etc/shadow取哈希做比对认证通过后按 passwd 里记录的家目录切换过去再启动记录的那个 shell。这条链路意味着两个结论。第一/etc/passwd的内容一旦有误认证根本走不到密码比对那一步直接拒绝。第二登录 shell 字段并不是建议值而是真正被执行的程序路径——sshd会去执行它。所以把系统服务账号的 shell 从/usr/sbin/nologin改成/bin/bash等于给这个账号开了后门只要密码或者密钥对得上就能进系统。这一点在安全审计里是高频扣分项。同样地sudo在做权限提升前也会确认当前调用者的 UID 能在 passwd 库里解析到这也是那类you do not exist in the passwd database报错的直接来源。1.3 为什么密码一定要待在 /etc/shadow/etc/passwd的权限是-rw-r--r--也就是 644root 可写、所有人可读。这个权限是必须的因为普通用户执行ls -l、id、whoami都需要读它。反过来说任何写进这个文件的内容都等于公开信息。/etc/shadow的权限通常是000或者640且属于root:shadow普通用户根本读不到。再加上 shadow 文件里的哈希经过了加盐处理虽然理论上依然可以被离线暴力破解但门槛被抬高了很多。这两份文件的分工就这么定下来了passwd 管你是谁、你住哪、你用什么壳shadow 管你的密码哈希是什么、多久过期、是否被锁定。提示有些老系统上你可能会看到 passwd 第二个字段是*或者!那表示该账号被禁止通过密码登录只能走其它认证方式或者干脆就是个不能登录的占位账号。2. 一行记录七个字段逐个咬碎/etc/passwd的每一行代表一个账号字段之间用英文冒号:分隔一行固定七个字段顺序不能乱也不能少。拿一行最典型的出来看root:x:0:0:root:/root:/bin/bash拆开就是序号字段名这一行的值含义1用户名root登录时输入的名字2密码占位x哈希在 /etc/shadow 里3UID0用户数字标识4GID0主组数字标识5GECOSroot备注/全名信息6家目录/root登录后的落脚点7登录 shell/bin/bash登录后执行的程序七个字段看着简单但是每一个都有值得展开的细节尤其是 UID 和登录 shell 这两个。2.1 用户名与密码占位符用户名字段在同一个文件里必须唯一长度一般限制在 32 个字符以内允许字母、数字、下划线、短横线通常不建议以数字开头。这里有个容易忽略的点用户名里不能出现冒号因为冒号是字段分隔符一旦出现就会把一条记录拆成多余的字段解析器会直接报错或者丢弃这一行。同理用户名里也不建议出现空格和中文虽然某些发行版能容忍但在跨系统同步账号比如 LDAP、NIS、集中认证时极易出问题。第二个字段现在的标准值是x表示密码哈希存放在/etc/shadow中对应的同名条目里。除了x之外你可能还会看到几种取值空值两个冒号贴在一起表示这个账号不需要密码就能登录。这在安全上非常危险某些 PAM 配置会直接允许空密码通过。*一个不可能与任何哈希匹配的占位符效果是该账号无法用密码登录。!或!!通常出现在 shadow 文件里表示账号被锁定。一串看起来像乱码的字符极老系统上遗留的哈希现在基本见不到了。我在做服务器加固的时候习惯性会扫一遍这个字段凡是空值或者不是x的都要拎出来单独确认用途很多历史遗留的测试账号就是这么被翻出来的。2.2 UID 的三段划分与它背后的权限逻辑UID 是整份文件里最核心的字段因为系统的权限判断只看这个数字不看用户名。这里必须强调一句话UID 等于 0 的账号就是超级用户不管它叫什么名字。所以如果你发现文件里有两个 UID 为 0 的条目比如除了 root 之外还有一个叫admin或者别的什么名字的那基本可以判定是留了后门必须立刻处理。UID 的取值范围在实践中被切成了三段不同发行版的边界略有差异区段用途典型例子备注0超级用户root有且应当只有一个1 - 999系统账号/服务账号daemon、bin、sshd、nginxRHEL 系常用 1-999Debian 系常用 100-9991000 及以上普通登录用户zhangsan、devops具体下限由 login.defs 控制这三段的划分不是摆设useradd在创建账号时会根据/etc/login.defs里的UID_MIN、UID_MAX、SYS_UID_MIN、SYS_UID_MAX来自动决定分配哪一段的数字。你执行useradd nginx或者带-r参数时系统会从系统账号区间里挑一个没被占用的数字执行useradd zhangsan时则从普通用户区间挑。还有一个特殊值值得单独记一下65534通常对应nobody这个账号。它的作用是不属于任何人的身份NFS 在开启 root squash 之后客户端的 root 请求会被映射成这个 UID避免远程 root 直接拿到服务器上的超级权限。有些系统里还会见到nfsnobody也是类似用途。再说一个非常实用的经验改 UID 是一个危险操作。因为文件系统里记录属主用的是数字你把某个用户的 UID 从 1001 改成 1005那么这个用户原来拥有的所有文件在新 UID 下就变成了陌生人ls -l显示成一串数字。usermod -u 1005 -m zhangsan这个命令在改 UID 的同时会递归修正家目录内文件的属主但家目录之外的、散落在/data、/opt里的文件它管不到得自己find / -uid 1001 -exec chown zhangsan {} \;去补。这种操作建议在维护窗口做别在业务高峰上手。2.3 GID 与 GECOS 字段的实际用途第四个字段 GID 是这个用户的主组编号。注意这里写的是主组不是所属的所有组。一个用户除了主组之外还可以加入若干个附加组那些信息记录在/etc/group里不在这个文件里。所以想完整看清一个用户的组关系正确做法是跑id zhangsan它会同时把主组和附加组都列出来。主组的设计初衷是方便文件共享同组的人创建的文件默认就归这个组所有方便协作。实际使用中很多人创建用户时会顺手建一个跟用户同名的组useradd默认行为就是这样这样每个用户都有自己独立的主组避免所有人共享一个组导致的权限混乱。第五个字段 GECOS 的名字来源挺有意思它来自早期与通用电气公司合作的操作系统项目当时这个字段是用来给finger命令展示用户信息的。它的内容可以再细分成用逗号隔开的五个子字段分别是全名、房间号、工作电话、家庭电话、其它备注。现在基本只有第一个子字段全名还有实际意义后面的房间号和电话在现代环境里早就没用了。写的时候要注意GECOS字段里不要出现冒号但可以出现逗号。如果你想写完整的五个子字段格式是张三,研发部,8021,13800000000,备注。实际运维里常见做法是只写全名或者干脆留空写不写都不影响登录。2.4 家目录与登录 shell两个最容易出问题的字段第六个字段是家目录的绝对路径。登录成功后shell 的当前目录会切换到这儿用户的个人配置文件.bashrc、.ssh/authorized_keys、.profile也都放在这里。如果这个路径指向的目录不存在登录过程本身可能成功但你会直接落在根目录/或者得到一个刺眼的警告。useradd加上-m参数才会自动创建家目录不加的话系统只写一条记录、不建目录这是新手很容易忽略的一个差异。需要注意的是系统账号的家目录通常是个不太一样的路径。比如nginx可能指向/var/lib/nginx或者/nonexistentsshd可能指向/var/empty/sshd。这些目录往往权限收紧、内容几乎为空因为它们本来就不承担个人工作区的角色。第七个字段是登录 shell它决定了账号能做什么。常见取值和含义如下shell 路径含义适用场景/bin/bashBash 交互式登录普通用户、管理员/bin/zshZsh追求体验的开发者/bin/sh通常是 dash 或 bash 的软链脚本兼容/usr/sbin/nologin拒绝登录打印提示后断开系统服务账号/sbin/nologin同上的老路径老系统/bin/false静默退出不打印任何信息需要完全静默的场合这里有个细节/usr/sbin/nologin和/bin/false虽然都能阻止交互登录但行为不同。前者会打印一段类似 This account is currently not available. 的提示后者什么都不说直接返回失败状态。做加固时我一般统一用nologin因为出问题时能给人一点线索不至于对着黑屏发呆。另外nologin只是阻止了交互式 shell 登录并不阻止这个账号被su、sudo -u或者服务进程以其它身份使用。如果你要的是彻底禁用还得配合锁定密码或者把账号本身删掉。3. 动手实操从查看到改动的完整流程理论讲完接下来走一遍真实操作。这一节的所有命令都在一台干净的虚拟机上跑过你可以照着复现。装虚拟机的过程这里就不展开了随便一个主流发行版都行我用的是一台最小化安装的系统。3.1 查看与过滤的正确姿势最直接的方式当然是cat但真正干活的时候我更推荐getent# 查看指定用户 getent passwd root # 查看全部等价于 cat但走的是统一的名称服务接口 getent passwd # 只看普通用户过滤掉系统账号 awk -F: $3 1000 $3 65534 {print $1\t$3\t$6\t$7} /etc/passwd为什么优先用getent而不是cat因为getent会按照/etc/nsswitch.conf里配置的顺序去查所有数据源。在只有本地账号的机器上两者结果一样但一旦接入了集中认证LDAP、SSSD、Winbind 之类/etc/passwd里只有本地账号getent却能把远程账号也查出来。如果你习惯用grep xxx /etc/passwd来判断一个账号是否存在在集中认证环境里会得到完全错误的结论——明明账号存在你却以为没有。提示nsswitch.conf里passwd:那一行的顺序决定了查找优先级常见的写法是files sss意思是先查本地文件、再查 SSSD。排查账号问题的第一步永远是getent passwd 用户名。再补充两个常用组合。想看某个 UID 对应哪个账号用getent passwd 1001想看某个用户到底属于哪些组用id 用户名。这两个命令配合起来账号的基本面貌就清楚了。3.2 新建一个用户并逐字段验证我用useradd手工造一个账号把常用参数都用上useradd -m \ -s /bin/bash \ -u 1600 \ -c Zhang San,RD,8021,13800000000 \ -G developers \ zhangsan逐个解释这些参数在做什么-m创建家目录路径默认/home/zhangsan。不加这个参数家目录字段照样会写进/etc/passwd但目录不存在。-s指定登录 shell。不指定的话会取/etc/default/useradd里SHELL的值很多发行版默认就是/bin/bash。-u手工指定 UID。不指定就自动分配在普通用户区间里挑第一个空位。-c写 GECOS 备注。注意内容里有逗号是可以的。-G加入附加组developers。这个组必须已经存在否则命令直接报错不会自动创建。最后那个zhangsan是用户名同时默认会创建一个同名的组作为主组。执行完之后立刻验证grep zhangsan /etc/passwd # 输出zhangsan:x:1600:1600:Zhang San,RD,8021,13800000000:/home/zhangsan:/bin/bash id zhangsan # 输出uid1600(zhangsan) gid1600(zhangsan) groups1600(zhangsan),1001(developers)对着输出核对一遍UID 是不是 1600GID 是不是同名组家目录存不存在shell 对不对。这里有个我踩过的坑值得说一下——-G指定的附加组在 GECOS 里是看不到的很多人对着 passwd 那一行反复看发现不了自己加错组。验证组关系必须用id。新账号建出来还没有密码这时候它其实处于一个能用但登不进去的状态。设置密码passwd zhangsan交互式输入两遍。想脚本化批量设置可以用echo 密码 | passwd --stdin zhangsan不过--stdin是 RHEL 系passwd的特有参数Debian/Ubuntu 上不支持跨发行版的写法是用chpasswdecho zhangsan:密码 | chpasswdchpasswd从标准输入读取用户名:密码格式的文本这个命令在主流发行版上都一致写自动化脚本时更省心。3.3 改 shell、改家目录、改备注的实操账号建好之后改动需求很常见usermod是主力工具。把账号禁止登录服务账号加固的标配操作usermod -s /usr/sbin/nologin zhangsan把家目录整体挪到数据盘usermod -d /data/home/zhangsan -m zhangsan-d改路径-m表示把老目录的内容一起搬过去。不加-m只改记录、不搬文件这是很多人迁移完发现个人配置全丢了的真实原因。另外搬家过程中原目录里的隐藏文件也会一起过去因为搬的是整个目录不是目录里的可见文件。改 GECOS 备注usermod -c Li Si,Operations zhangsan改 UID慎用前面说过原因usermod -u 1700 zhangsan这个命令会顺带修正家目录内文件的属主。执行完记得用find / -uid 1600 -ls扫一遍看看还有没有散落在别处的孤儿文件需要手动处理。改主组usermod -g newgroup zhangsan这里要特别注意-g是改主组-G是设置附加组列表。而且-G的行为是覆盖而不是追加usermod -G a,b zhangsan会把 zhangsan 的附加组整体替换成 a 和 b原来在 c 组里的身份会丢掉。想追加一个组而不影响已有的得用usermod -aG newgroup zhangsan这个-a参数漏掉是运维里最常见的权限怎么突然没了的来源之一我自己也犯过。3.4 用 vipw 和 pwck 安全地编辑与校验有人图省事直接用vim /etc/passwd改我强烈不推荐原因有三个。一是这个文件没有锁机制你这么编辑的同时别人或者某个自动化脚本执行useradd两边同时写就可能互相覆盖少一条记录二是手写容易出格式错误冒号多一个少一个整行就废了三是改完之后无法自动同步/etc/shadow和/etc/group。正确做法是用专门的编辑器vipw它做的事情是给文件加锁、创建临时副本给你编辑、保存时做语法检查、确认无误后原子替换回去。配套的还有vigr用来编辑组文件vipw -s用来编辑 shadow 文件。编辑完或者批量导入账号之后做一次一致性检查pwckpwck会逐行检查 passwd 的字段数量、UID 唯一性、家目录是否存在、shell 是否在/etc/shells白名单里等等把可疑的行报出来。组文件对应的检查工具是grpck。这两个命令在系统加固和故障排查时都非常好用我一般在做完批量账号操作后都会跑一遍。还有一组命令值得记住# 查看账号的密码状态是否设置、是否锁定、上次修改时间 passwd -S zhangsan # 查看密码策略有效期、最短使用天数、警告天数等 chage -l zhangsanpasswd -S的输出第二列如果是P表示密码已设置L表示锁定NP表示无密码。这几个状态跟 passwd 文件的第二个字段是联动的理解了就能快速判断账号到底能不能用密码登录。4. 跟 /etc/passwd 绑在一起的那几个文件单独看/etc/passwd容易只见树木不见森林实际运维里它总是和一个文件家族一起出现。搞清楚它们之间的分工很多问题的定位速度会快一个量级。4.1 /etc/shadow 与 /etc/group 的分工/etc/shadow的每一行有九个字段跟 passwd 一一对应同一个账号内容包括密码哈希、上次修改时间、最短使用天数、最长使用天数、警告天数、宽限天数、账号失效日期、保留字段。字段多了之后密码策略就能做得非常细比如强制 90 天换一次密码、过期前 7 天开始提醒。/etc/group的每一行是四个字段组名、组密码占位、GID、成员列表。注意这里有个容易混淆的地方——用户的主组信息不在这个文件的成员列表里。比如 zhangsan 的主组是 zhangsanGID 1600在/etc/group里那一行是zhangsan:x:1600:冒号后面是空的。只有附加组的成员列表里才会出现用户名。这就是为什么id命令比翻 group 文件更可靠。这三个文件的关系可以用一句话概括passwd 定义身份和登录环境shadow 保存凭证和时效策略group 管理组关系和成员。改动其中一个往往需要同步考虑另外两个。4.2 /etc/login.defs 与 /etc/default/useradd 决定默认行为useradd不带参数时凭什么决定 UID 从哪开始分配、家目录建在哪、默认 shell 是什么答案在这两个配置文件里。/etc/login.defs里跟账号相关的关键项配置项典型值作用UID_MIN1000普通用户 UID 下限UID_MAX60000普通用户 UID 上限SYS_UID_MIN201系统账号 UID 下限SYS_UID_MAX999系统账号 UID 上限CREATE_HOMEyes是否默认创建家目录PASS_MAX_DAYS99999密码最长有效期/etc/default/useradd里则定义了家目录模板路径、默认 shell、默认所属组策略、是否创建邮箱文件等。有一段时间我在做等保相关的整改需要统一全公司服务器的 UID 分配区间和密码有效期改的就是这两个文件改完再配合配置管理工具推到所有机器上。提示这两个文件在不同发行版里的默认值差异相当大。Debian/Ubuntu 的系统账号区间习惯从 100 起RHEL 系从 201 起跨发行版做统一规范时如果不注意很容易出现 UID 冲突。4.3 nsswitch.conf 与 getent 的配合关系/etc/nsswitch.conf决定了系统去哪些地方找账号信息。典型的passwd:行可能是files systemd、files sss、或者compat。这一行的顺序就是查询顺序找到即止。理解这层关系能解释很多灵异现象。举个例子某台机器接入了集中认证本地/etc/passwd里也有一个同名账号那么登录时到底用哪一份答案是看 nsswitch 的顺序。如果files在前本地那份优先集中认证里的密码策略就被绕过了这在安全合规检查里是个典型的失分点。反过来说如果sss在前本地账号被遮住你用passwd改本地密码会发现改了个寂寞。我的习惯是排查账号问题时的固定三步先getent passwd 用户名确认系统视角下这个账号存在且信息正确再getent passwd | wc -l看看总条目数是否合理有没有爆炸或者空掉最后才去看/etc/passwd这个文件本身。顺序反过来容易走弯路。5. 踩坑实录与常见故障速查这一节是我这些年攒下来的真实故障和应对方式大部分在网上搜不到现成答案但一旦遇上会很耽误时间。5.1 账号类故障速查表现象大概率原因第一步排查命令sudo 报当前用户不存在于密码数据库账号条目被删或 UID 解析不到id $(whoami)登录成功但落在 / 目录家目录字段指向的路径不存在getent passwd 用户名然后ls -ld 路径useradd 报 UID 已存在手工指定了重复 UIDawk -F: $3UID /etc/passwd服务账号能 SSH 登录进来shell 被误改成 /bin/bashgetent passwd 服务名查第七字段ls -l 显示一串数字而不是用户名文件的 UID 在系统里没有对应条目getent passwd 数字集中认证用户改了本地密码不生效nsswitch 顺序导致查的是远程源grep ^passwd /etc/nsswitch.conf某账号加了附加组但权限没变化用了 -G 覆盖而不是 -aG 追加id 用户名手工编辑后某行账号消失行格式错误或末尾缺换行符pwck容器内应用读用户信息报错容器内 passwd 缺对应 UID 条目docker exec 容器 getent passwd这张表里的每一条我都真实遇到过其中两个值得展开讲。5.2 三个真实场景的排查过程场景一一行漏掉的换行符引发的连环事故。有台老服务器在做账号清理时同事用手工方式编辑了/etc/passwd删掉了中间的一行保存时编辑器没有在文件末尾补上换行符。之后新增账号useradd把新记录直接追加到了最后一行末尾结果最后两个账号粘成了一行字段数直接超标。现象是这个账号能建出来但登不进去而且前面那个没被删的账号也莫名失效了。定位过程很快pwck一跑就把那行揪出来了。教训是永远不要用普通编辑器碰这个文件用vipw。场景二NFS 共享目录的权限集体错乱。一套集群里计算节点挂载了存储节点的数据目录某天做完账号规范化之后所有计算结果文件都变成了nobody所有应用直接写不进去。原因很直接存储节点上某个用户的 UID 从 1003 被改成了 1005而计算节点上还是 1003网络文件系统只认数字不认名字两边对不上号落到nobody上了。解决办法是把两边 UID 强制对齐并且在/etc/login.defs里把分配区间锁死避免以后再出现类似的漂移。这也是为什么我坚持在集群环境里手工指定 UID而不是让系统自动分配。场景三容器内 Java 应用启动报找不到用户。有个基于精简镜像打包的 Java 服务启动日志里一直有一行关于无法获取用户信息的警告功能上不影响但看着难受。根源是精简镜像里/etc/passwd只保留了极少数条目容器以某个 UID 运行时这个 UID 在文件里查不到名字System.getProperty(user.name)拿到的是空值。处理方式是在构建镜像时用useradd显式建一个用户让记录写进/etc/passwd而不是在docker run时用--user指定一个裸数字。5.3 改动这个文件时必须守住的几条线做了这么多年账号维护我给自己定的规矩是这几条分享出来供参考。第一UID 一旦确定就不要轻易改。文件属主、定时任务、服务配置、数据库连接串里都可能硬编码或者间接依赖这个数字改动的连锁反应远超预期。确实需要改的时候改完必须扫全盘找孤儿文件。第二系统服务账号的 shell 一律设成 nologin并且定期复查。这个检查很容易自动化一行awk -F: $31000 $7!~/nologin|false/ {print $1,$7} /etc/passwd就能把所有违规的列出来塞进巡检脚本里每周跑一次。第三杜绝 UID 为 0 的第二个账号。定期执行awk -F: $30 {print $1} /etc/passwd输出应当只有root一个。第四批量操作前先备份。cp -a /etc/passwd /etc/passwd.bak.$(date %F)这一行花费不到一秒但能在关键时刻救回一台机器。如果是虚拟机或者物理机做账号大规模调整之前打个快照更稳妥出问题直接回滚比逐行修复快得多。第五密码空值的账号必须清零。检查命令是awk -F: $2 {print $1} /etc/passwd输出为空才安心。这类账号在某些宽松的 PAM 配置下真的能空密码登进去。第六改完之后永远跑一次 pwck 和 grpck。这两个工具花不了两秒钟但能挡掉绝大多数低级错误。养成习惯之后账号相关的线上事故率会明显下降。关于这个文件的后续扩展方向我个人觉得有两块值得深挖。一块是集中认证体系下的账号统一管理本地 passwd 文件逐渐退化成最后兜底的角色怎么设计这套兜底策略、怎么保证本地和远程账号不打架是个挺有嚼头的题目。另一块是容器和不可变基础设施场景下的账号模型传统主机的账号管理思路在那里基本失效需要另一套做法。这两个方向我后面会另外整理。