ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux /etc/shadow 完全解析:密码哈希与账户安全策略实战

Linux /etc/shadow 完全解析:密码哈希与账户安全策略实战 如果你在 Linux 上折腾过用户密码应该会有一种感觉/etc/passwd 只是个“户口本”真正管密码的地方其实是 /etc/shadow。很多朋友第一次 cat /etc/passwd看到密码字段写着 x会以为密码被隐藏了其实准确说是被挪走了。系统里所有用户的密码哈希、密码有效期、账户生命周期都写在 /etc/shadow 这个“影子文件”里。这篇文章我按实际运维的视角把它从字段到操作、从原理到踩坑完整拆一遍适合刚接触 Linux 用户管理、准备考认证或者正在做日常运维排查的同学。1. 为什么当初要把密码从 passwd 里挪出去1.1 passwd 文件的历史包袱全世界可读哈希裸奔早年 Unix 时代/etc/passwd 要同时干两件事一是给系统提供用户名、UID、GID、家目录、登录 shell 这些身份信息二是存放用户密码哈希。问题在于这个文件必须对所有人可读。你随手敲一个ls -l /home系统得能显示出每个文件属于哪个用户getent passwd username也得能被普通用户执行否则大量日常操作都没法进行。这就形成了一对结构性矛盾密码哈希和身份信息放在同一个文件里而这个文件又必须“全世界可读”。攻击者只要拿到一份可读的 passwd 文件就能把里面的哈希拿回家慢慢跑字典。早些年机器性能弱破解还挺费劲等到硬件价格降下来离线跑哈希就成了常规操作。你这边还没察觉到任何异常密码可能已经被别人算出来了。解决办法就是拆分身份信息继续留在 /etc/passwd密码哈希挪到另一个文件里也就是 /etc/shadow。挪走之后/etc/passwd 的密码字段不再放哈希而是放一个字母 x。这里的 x 不是密码也不是随机占位符它是一条“指针”告诉你真正的哈希已经被挪进 shadow 了。1.2 shadow 的锁门方式权限、属主和“影子”名字的由来/etc/shadow 默认权限是 640属主 root属组 shadow。640 意味着只有 root 能读写shadow 组的成员只能读其他用户一概没有权限。为什么还要留一个只读的组权限因为有些场景需要非 root 进程读取 shadow 做校验某些 PAM 模块或自定义认证服务需要读到影子文件把这些服务账户加入 shadow 组就能在不给 root 的情况下完成读取。这比直接设成 600 更灵活。“影子”这个叫法特别形象/etc/passwd 里每个用户有一行/etc/shadow 里对应的用户也有一行结构几乎是跟着 passwd 走的。像一个实体在灯光下投出的影子实体在明处影子在暗处。灯光一关别人就只能看到影子摸不到实体。早期不同 Unix 系统的 shadow 实现五花八门有的叫 /etc/security/passwd有的直接不叫 shadow。但最后大家的方向统一了密码哈希不能放在任何人可读的文件里。而且 Linux 的 shadow 机制后来还扩展出了密码有效期、账户到期日等一系列策略字段这在最早的 passwd 时代是想都不敢想的。2. 九字段逐段拆解一个 shadow 行到底是什么打开 /etc/shadow你会发现每一行对应一个用户字段之间用冒号分隔一共 9 个字段。规范格式长这样dev01:$6$hY7QdR3nQZvMxKcW$xJ0l8WpZ...:19001:0:90:7:30:19231:下面把这 9 个字段逐个拆开讲。2.1 字段1和字段2登录名与哈希串第一个字段是用户名和 /etc/passwd 里的用户名必须严格一致。这里要提醒一点不要手动去两边改名字很容易造成“passwd 里有这个人但 shadow 里没有”或者反过来最后用户登录不了getent 显示也异常。真想改名用usermod -l走正规流程。第二个字段是密码哈希串整个文件的核心。哈希串通常以$算法id$盐值$哈希值的形式存在$6$oIW0dFrm0N0vENoY$JnHzoXTe0YQjlRxTGVvBcpt3Lw7c3lWYUfyO74qReGStY5s2w6VxH2qQaO95r4y9Qb3Mf0HsE1qQb0pU6jS0lLmKp1eDcVtF9nJQ0其中$6$后面那段oIW0dFrm0N0vENoY是盐值再往后是迭代运算后的哈希值。不同发行版默认算法不一样识别方式就看第一个数字或字母算法标识含义常见场景$1$MD5老系统现在基本淘汰$2a$/$2b$/$2y$Blowfish/bcrypt部分 BSD 系或自定义环境$5$SHA-256中等强度$6$SHA-512大多数 RHEL/CentOS 7/8 默认$y$yescryptRHEL 9、较新 Debian/Ubuntu 默认$argon2id$/$argon2i$Argon2部分新系统或自定义编译环境这里有一个特别实际的坑哈希算法不兼容。旧系统比如 CentOS 7 上的 glibc/pam读不了$y$开头的哈希新系统能兼容旧的$6$反过来不一定行。你从新机器上把 shadow 整行拷到旧机器用户密码会直接验证失败。这个案例我后面详细说。除了标准哈希串第二字段还可能看到这些特殊值!!账户被锁定且从未设置过密码常见于刚创建还没初始化密码的用户。!开头的哈希串用户原本有密码后来被passwd -l或usermod -L锁定。锁定操作其实是在原哈希前面追加了一个感叹号原哈希还在只是被临时“禁用”了。*系统账户或服务账户表示该账户不允许密码登录密码被“封死”了。空字段完全没设置密码。如果 SSH 还开了 PermitEmptyPasswords后果会很严重任何环境我都不建议开。如果你需要手工生成一个可用的哈希来替换第二字段可以用 openssl 临时顶一下openssl passwd -6 -salt 随机盐值 明文密码输出的就是$6$...$...格式可以放进 shadow。但注意这只是应急手段批量场景强烈建议用chpasswd这类命令处理。2.2 字段3到字段8密码生命周期的时间策略从第三个字段开始全是和时间相关的策略控制字段3上次修改密码的日期从 1970-01-01 开始计算的天数。比如 19001 表示改密发生在 1970 年之后第 19001 天。字段4最短修改间隔。两次改密之间至少隔多少天0 表示随时可以改。字段5最长有效期。密码从上次改密算起最多能用多少天超过就必须修改。常见默认是 99999代表几乎永不过期。字段6过期前警告天数。在密码到期前的多少天开始提醒用户“密码要过期了”。常见默认是 7。字段7不活跃宽限天数。密码过期之后用户一直没改密系统还会给一个宽限期超过这个天数账户直接被禁用。0 表示不启用。字段8账户绝对过期日期和密码有效期是两码事。这个针对的是整个账户的生命周期比如临时员工的账号设到某个日期就自动失效。0 或未设置表示不限制。字段3 是运维里用得最多的也是最容易出问题的。“强制用户下次登录改密码”最标准的方法就是把这一位改成 0chage -d 0 dev01执行后 dev01 下次登录就必须改密码。很多人会问为什么不直接在 shadow 文件里把第三字段改成 0可以但前提是你得保证整个文件格式正确。能用命令解决的事真没必要去动文件。2.3 字段9预留位的用途和惯例第九个字段是保留字段绝大多数系统里都是空的末尾看起来就是一个孤零零的冒号。有些系统会在这一位放 ACL 或者未来功能标记但主流 Linux 发行版里基本没有实际作用。需要注意这个字段不能省省略可能导致某些工具解析行时出错。所以看到行尾有个冒号是正常的。2.4 被锁账户的哈希串感叹号和星号的区别第二字段的特殊值容易混淆我单独拎出来说。锁定分两种。一种是账户从未设置密码或者密码被passwd -d清空了哈希字段可能是空的或者!!。另一种是用户本来有密码被passwd -l锁定系统在哈希串前面加了个感叹号。注意原哈希并没有被删除所以解锁用passwd -u时去掉感叹号密码能恢复原样。如果你用了别的操作把整个哈希清掉那就真的找不回来了。*通常出现在系统账户上比如 bin、daemon、sync。含义是该账户无法通过密码登录但系统服务和进程可以以该身份运行。这类账户不是“被锁定”那么简单它们压根就不该有可用的登录路径。3. 那些用“天数”计数的字段换算才是真的坑3.1 天数怎么来的怎么看回去shadow 文件里所有日期字段都不是“2025-03-14”这种格式而是一个整数从 1970-01-01 00:00:00 UTC 开始经过的天数。Unix 时间戳按秒计数shadow 按天计数本质是一样的系统。正向换算把某个天数变成日期date -d 1970-01-01 19001 days %Y-%m-%d反向换算算今天对应的天数echo $(( $(date %s) / 86400 ))date %s是当前系统时间戳秒除以 86400 再取整就是 shadow 里今天对应的天数。这条命令经常用来判断某个时间戳到底指向哪一天。如果觉得命令行换算太麻烦直接用一个更直观的方式chage -l dev01这条命令会把 shadow 里的时间戳翻译成标准年月日并列出“上次改密时间”“密码过期时间”“账户过期时间”。排查问题的时候先用 chage -l 看一眼比手算快得多。3.2 时间戳为 0 时的诡异行为字段3 为 0 的含义是“从未发生过密码修改”实际表现就是用户下次登录时系统强制要求修改密码。对新账号和刚重置过的密码这是常见需求。但如果你不小心给一个只用 SSH key 免密登录的服务账号执行了chage -d 0下次密钥登录时系统也会强制它走交互式改密登录流程直接卡住。我见过有人在 CI 流水线上因为这个排查了一下午。字段8 为 0 通常表示“账户没有设置绝对过期时间”但某些版本的 PAM 对 0 的解释存在差异有的 PAM 会把 0 当成“立即过期”。所以如果真想表达“永不过期”用chage -E -1比手动把字段改成 0 稳妥。这个坑在不同发行版上表现还真不一样。3.3 使用时间戳时最容易犯的几个错第一把天数当成秒数或者把秒数当成天数填进去。比如想把到期日设成 2025-12-31手一抖把 19231 写成了 192310000用户账户会瞬间进入过期状态。第二修改时间戳时没考虑系统时区。shadow 的换算是基于 UTC 的本地时区和 UTC 不同可能导致边界日期差一天大多数场景不影响但遇到严格的安全策略审计时可能踩雷。第三手动改文件时破坏了行的格式比如多打了一个冒号或者删错字段。这是最危险的PAM 读取到语法错误的条目时会直接拒绝整个账户的认证而且报错信息往往很隐晦根本看不出是文件格式问题。4. 实操中管理 shadow 的正确姿势4.1 查看 shadow 和用户密码状态普通用户执行cat /etc/shadow会得到 Permission denied这是正常现象。用 root 查看没问题或者用sudo cat /etc/shadow。如果不想在截图或者演示环境里暴露哈希可以用awk -F: {print $1} /etc/shadow只看用户名列表。查看某个用户密码状态最简洁的是passwd -S dev01输出类似dev01 PS 2025-01-10 0 90 7 30不同发行版略有差异含义依次是用户名、密码状态PS 表示可用密码LK 表示锁定NP 表示无密码、上次改密日期、最短间隔、最长有效期、警告天数、宽限天数。这个命令适合脚本里做判断。4.2 chage 命令安全改密码策略chage 是管理 shadow 生命周期最主力的命令我常用的几个操作# 查看用户密码生命周期 chage -l dev01 # 设置密码 90 天过期提前 7 天提醒最短间隔 0 chage -M 90 -W 7 -m 0 dev01 # 强制用户下次登录改密 chage -d 0 dev01 # 设置账户到期日 chage -E 2025-12-31 dev01 # 取消账户到期限制 chage -E -1 dev01注意chage -M 90只改策略不会动已有的密码哈希。它修改的是 shadow 里的字段5。还有一点chage -l输出的Last password change是从 shadow 字段3 换算出来的如果你手动改过文件这里显示的日期会不一致排查时可以作为对照。4.3 vipw -s 和其他安全编辑入口如果真的需要直接编辑 shadow 文件不要用 vim 或 sed 硬上用vipw -s。vipw 打开编辑器之前会先给文件加锁防止和其他进程的并发写操作冲突保存后还会做基本的格式校验。如果只输入vipw不加-s编辑的是 /etc/passwd。这两个文件都建议不要用普通编辑器碰除非你确定当前环境没有其他进程在改用户信息。4.4 pwconv / pwunconv 的用途和风险pwconv 的作用是根据 /etc/passwd 里的用户列表同步生成或更新 /etc/shadow。什么场景会用比如从旧系统直接拷贝了 passwd 文件过来但没带 shadow或者手工在 passwd 里加了一行用户想让它出现在 shadow 里。反过来pwunconv 会把 shadow 里的密码哈希合并回 /etc/passwd然后删除 shadow 文件。这个操作在极老的系统、某些单用户救援模式或者需要临时绕过 shadow 机制时才可能用到。一旦执行完 pwunconv系统就退回“哈希公开可读”的老路非常危险。除非能明确说出现在在干什么否则不要碰这个命令。5. 我踩过的 shadow 相关的坑5.1 案例一把哈希从新版拷到旧版密码验证失败背景是这样公司有一批老旧的 CentOS 7 服务器为了统一账号我想把新上线的 RHEL 9 服务器上已经设置好的用户密码哈希同步过去。操作不算复杂把 shadow 文件里对应的那行复制过去然后测试登录。结果所有同步过去的账号都报 Permission denied。排查过程先用 root 登录老机器执行getent shadow dev01确认哈希和源机一致。然后我用su - dev01测试依然失败。我注意到新机器生成的哈希前缀是$y$这是 RHEL 9 默认的 yescrypt 算法。CentOS 7 的 pam_unix 根本不认识这个格式直接把整串哈希当成非法内容处理密码自然无法验证。老系统能正常识别的通常是$6$这类 SHA-512差异不在哈希长度而在算法标识。修复在 CentOS 7 上用passwd dev01重新给这些用户设置密码让系统按老环境支持的算法生成新哈希。之后我专门定了规矩跨系统同步 shadow必须确认目标系统 pam 和 glibc 支持源系统的哈希算法不能看前缀差不多就认为兼容。5.2 案例二脚本直接改 shadow 行把字段改错了还有一次需要把一批服务器的用户密码策略从永久改成 180 天过期。图省事我用 sed 直接在 shadow 文件里替换字段5。脚本逻辑是匹配到某个用户那行把最长有效期字段改成 180。执行完之后好几个用户登录就开始提示密码过期而且自己用 passwd 改密码还会报错。排查过程先chage -l看这几个用户发现最短修改间隔字段4变成了 180而不是最长有效期字段5。也就是说我的正则匹配错了位置。原因是这些用户的第三字段有几位是 0而我的表达式用通用字符跳字段时没有考虑空值和 0 的情况匹配整体错位。加上我用了-i原地修改原文件没有留下任何操作日志起初根本不知道改坏了哪些行。修复从备份恢复 shadow 文件改用chage -M 180批量处理。chage 走的是系统接口不会去解析哈希串也不会因为字段值不同而错位。从那以后涉及 shadow 策略调整我优先用 chage 和 passwd只有少数极端场景才考虑脚本直接写文件而且必须先把测试机跑通。5.3 事后总结的检查清单每次动 shadow 之前我会按这个清单过一遍先备份cp -a /etc/shadow /etc/shadow.bak.$(date %Y%m%d)属性必须保留。首选命令接口chage、passwd、usermod少直接编辑文件。如果必须编辑文件用vipw -s不要用普通编辑器硬开。编辑前确认当前系统时间和时区避免时间戳换算出错。跨系统同步哈希时先确认目标系统 PAM 支持哈希算法前缀。操作完用getent shadow或chage -l验证预期字段是否变化。对服务账号尤其小心不要在没有确认是否支持交互改密的情况下执行chage -d 0。这套清单看着麻烦但每次做账号策略调整最多多花五分钟却能挡住大多数把系统搞到无法登录的操作。尤其是“命令优先”这条我最初也嫌 chage 写命令麻烦总想一把 sed 直接改文件直到那次把最短间隔和最长有效期改错位才彻底改掉这个习惯。如果你也是第一次接触 shadow建议先在一台测试机上把chage -d 0、passwd -l、chage -E这些操作挨个试一遍看看 shadow 里每个字段是怎么变化的比背十遍字段说明都管用。
返回列表