开工前,先聊聊改密这件事为什么值得说道
做运维和开发的朋友,几乎每天都在跟 Linux 打交道,但说起 "passwd 命令",很多人第一反应是:不就是改个密码吗?有什么好讲的。可实际工作里,围绕密码修改翻车的案例一点都不少:有人批量改密把 root 锁了,有人在脚本里明文传密码被审计揪出来,还有人忘记了 passwd 命令在非交互模式下根本不吃标准输入这一茬,导致自动化任务静默失败。这些坑,我这些年都踩过,也看身边的同事踩过。
这篇文章就围绕 passwd 命令本身,把它的参数、交互逻辑、非交互用法、常见报错、安全建议一条条拆开讲透。适合刚接触 Linux 的运维新人,也适合那些写惯了自动化脚本、却从没深究过 passwd 底层行为的开发者。读完之后,你不仅能手动把密码改利索,还能在脚本里安全、稳定地完成批量改密,遇到锁账户、密码过期这类问题也能从容排查。
先说个基本认知:passwd 是一条 setuid 程序,它修改的是 /etc/shadow 文件里的密码哈希字段。这个文件普通用户读不了,但 passwd 能以 root 身份帮你完成写入。理解了这一层,后面所有参数和行为就都顺理成章了。
1. 命令基础与核心参数拆解
1.1 passwd 到底做了什么
passwd 命令的全称是 "password",它的核心职责就是更新用户的认证信息。在传统 Linux 系统中,用户密码并不是明文存储在系统里的,而是经过哈希算法处理之后,存放在 /etc/shadow 文件中。这个文件每一行对应一个用户,字段之间用冒号分隔,其中第二个字段就是密码哈希值,形如$6$salt$hash这样的字符串。$6$表示使用 SHA-512 算法,这是大多数现代发行版的默认选择。
当你执行passwd修改密码时,系统会先验证当前用户的旧密码(如果当前用户是 root,则可以跳过这一步),然后要求输入两次新密码,确认一致后,passwd 会调用底层的 crypt 库函数,生成新的哈希值并写回 /etc/shadow。整个过程对外表现是"交互式问答",但本质上是一次"读取-验证-哈希-写入"的完整事务。
理解这个流程对排错特别重要。"为什么我改了密码,但 SSH 登录还是提示密码错误?"这种问题,99% 是因为写入环节没成功,或者写入的哈希算法与系统认证配置不匹配。这不是玄学,是机制。
1.2 常用参数逐个说
先列一个速查表,把高频参数和用途摆出来,后面再展开讲容易踩坑的点。
| 参数 | 功能 | 典型场景 |
|---|---|---|
-S | 查看账户密码状态 | 快速确认账户是否锁定、密码是否过期 |
-l | 锁定账户 | 临时禁用某个账户登录 |
-u | 解锁账户 | 解除锁定,恢复登录 |
-d | 删除密码 | 使账户变为无密码状态,仅建议用于特殊测试环境 |
-e | 强制密码过期 | 下次登录时必须先修改密码 |
-x | 设置密码最大有效天数 | 密码到期前必须更换 |
-n | 设置密码最小修改天数 | 两次改密之间的最小间隔 |
-w | 设置到期前告警天数 | 提前提醒用户密码即将过期 |
-i | 设置宽限期 | 密码过期后仍允许登录的天数 |
--stdin | 从标准输入读取密码 | 脚本和非交互改密的核心参数 |
这里重点提醒一下--stdin。这个参数在绝大多数发行版中默认是不包含的,需要额外确认。它的作用是让 passwd 直接从标准输入读取一行作为新密码,而不是走交互式提示。正因为这个特性,它成了自动化脚本的宠儿,但也正因为它是"从标准输入读密码",如果使用时不注意管道和进程列表的可见性,密码就可能在命令行参数或 shell 历史中暴露。具体防泄漏方案,我在第四节专门聊。
1.3 不带参数的交互流程
日常手动改密,直接敲passwd就行。系统会让你先输入当前密码(非 root 用户),然后输入新密码,再重复一次确认。有一个细节很多人没注意:新密码输入时,终端不会回显任何字符,连星号都不显示。这不是卡住了,而是设计如此,防止旁人偷窥密码长度。
如果你是在 root 下执行passwd 用户名,则可以跳过旧密码验证,直接设置新密码。这个差异背后是 Linux 权限模型的基本逻辑:root 拥有对 /etc/shadow 的写权限,因此不需要证明自己是"原密码持有者"。
实际操作中,我建议所有人在首次登录一台新机器时,做一次"空跑"演练:用passwd -S看一眼自己的账户状态,用passwd -e强制让自己下次登录改密,然后退出重登。这套动作做完,你对"密码过期"和"密码状态查看"这两个概念就有了肌肉记忆。
2. 核心原理:影子文件与密码存储机制
2.1 /etc/shadow 的结构,每个字段都别放过
很多人知道 /etc/shadow 是存密码的,但没仔细看过它的结构。为了后面排查问题方便,我在这里拆开讲一遍。每一行由九个冒号分隔的字段组成,以 root 用户为例:
root:$6$abc123$def...:19000:0:99999:7:::字段含义依次是:
- 用户名
- 密码哈希值(如果是
!或!!开头,表示账户被锁定或无密码) - 最后一次修改密码的日期,单位是"从 1970-01-01 起的天数"
- 两次修改密码之间的最小间隔天数
- 密码最大有效天数(99999 表示永不过期)
- 密码过期前告警天数
- 密码过期后的宽限天数
- 账户失效日期(留空表示不失效)
- 保留字段,通常为空
当你在脚本里或排障过程中发现密码"改不动"时,第一件事就应该是查看这九个字段,确定是哈希值没写进去,还是过期策略字段出了问题。用awk或者cut就能快速提取,没必要非用专门的工具。
2.2 哈希算法与 $6$、$5$、$y$ 前缀
上文提到/etc/shadow中的哈希值带有算法前缀,这是 passwd 能否正常工作的关键。常见的前缀包括:
$1$:MD5,老系统还在用,安全性已经不足$5$:SHA-256,部分系统默认$6$:SHA-512,多数现代 Linux 发行版的默认选项$y$:yescrypt,一些新发行版已经切换到这个算法
不同发行版对默认哈希算法的选择不同,例如某些新版本系统默认采用 yescrypt,而另一些仍停留在 SHA-512。如果你手动生成哈希并写入 /etc/shadow,必须确保算法与系统认证配置一致,否则会出现"密码看起来改了,但登录总失败"的诡异现象。
更稳妥的做法是:永远使用 passwd 命令来改密,而不是手动编辑 /etc/shadow。手动编辑文件也许能绕过一些限制,但也很容易把哈希格式写错,或者破坏文件的权限和 SELinux 上下文,给自己挖一个更大的坑。
2.3 权限问题:为什么普通用户能改自己的密码
这个问题是我刚入行时一直没想明白的:/etc/shadow 权限明明是 640 或 600,普通用户连读都读不了,怎么还能改自己的密码?答案在于 passwd 命令本身的 setuid 位:-rwsr-xr-x中的s表示这个程序在运行时,会以文件属主(root)的身份运行,而不是以调用者的身份运行。
也就是说,普通用户执行 passwd,实际是让 passwd 这个程序临时获得了 root 权限,然后由它来更新 shadow 文件。这个设计是出于安全考虑的"最小授权":用户只能通过 passwd 这个受控入口来修改自己的密码,无法直接读取或篡改其他用户的哈希值。
这也解释了为什么你不能直接修改其他用户的密码:普通用户执行passwd 用户名,系统会明确提示"只有 root 才能指定用户名"。这条权限边界是内核和程序共同维护的,不要试图用sudo以外的办法去绕过它。如果你确实需要为其他用户改密,请在 root 或 sudo 环境下操作。
3. 实操全流程:从手动改密到批量脚本落地
3.1 手动修改密码的标准流程
手动操作看似简单,但步骤不能乱。我建议按这样的顺序执行,减少误操作的概率:
- 登录系统后,先用
whoami确认当前身份。 - 执行
passwd(修改自己的密码)或sudo passwd 用户名(修改指定用户密码)。 - 根据提示输入当前密码(如果是 root 改别人则可跳过)。
- 输入新密码,系统提示强度不够时会警告,但不会强制拒绝(取决于 PAM 配置)。
- 再次输入新密码,确认一致后,系统会输出类似
passwd: all authentication tokens updated successfully的信息。
这里有一个值得养成的习惯:改完密码后,立刻开一个新 SSH 会话,用新密码登录验证一次,确认无误后再关闭旧会话。因为一旦密码改错或遗忘,旧会话又断了,你就只能通过控制台或带外管理来恢复了。这个习惯帮我避免过至少三次"改完密码把自己锁外面"的事故。
3.2 非交互改密:脚本中如何使用 passwd
自动化脚本里没法人工输密码,最常见的做法是使用--stdin参数。基础写法如下:
echo "NewPassword123" | passwd --stdin zhangsan这种写法能跑通,但密码会出现在 shell 历史记录、进程列表和其他用户的临时文件中,安全风险很高。更稳妥的改进方案是把密码存放在一个权限受限的文件里,然后通过重定向传入:
passwd --stdin zhangsan < /root/.secret/.passwd_zhangsan把密码文件放在 root 目录下,权限设为 600,这样进程列表里不会暴露密码,历史记录也干净了。
如果你使用的是 Debian/Ubuntu 系系统,passwd默认可能不支持--stdin,需要借助chpasswd命令来实现同样的目标:
echo "zhangsan:NewPassword123" | chpasswdchpasswd 从标准输入读取"用户名:密码"格式的文本,逐行处理,非常适合批量场景。
3.3 批量修改多个用户密码
批量场景下,推荐用 chpasswd 一次性处理多个用户,效率远高于循环执行 passwd。准备一个文本文件,每一行是"用户名:新密码",例如:
zhangsan:Passw0rd@2024 lisi:NewPass@123 wangwu:Welcome@2024然后执行:
chpasswd < /root/user_passwd.txt命令执行完毕后,可以用下面的循环验证每个账户的状态:
while read user; do echo "== $user ==" passwd -S "$user" done < user_list.txt批量操作最大的风险是"一刀切":如果其中某个用户名写错了,或者密码包含特殊字符在读取时被转义,可能导致部分账户改密失败却没有任何提示。因此,批量执行后必须逐一核对状态。我的做法是执行前先打印一份待处理清单,执行后再对比前后状态,确保没有漏网之鱼。
3.4 强制过期与登录策略联动
很多企业安全策略要求新员工首次登录时强制改密,这个需求用passwd -e就能优雅实现:
passwd -e zhangsan这条命令会把该用户 shadow 文件中"最后一次修改密码日期"字段归零,下次该用户登录时,系统会提示密码已过期,必须先设置新密码才能继续操作。等新员工设置完密码,这个"过期"状态就会被自动清除。
与passwd -e配套的策略参数还有:
# 设置密码最长使用90天,提前7天告警 passwd -x 90 -w 7 zhangsan # 设置两次改密至少间隔3天 passwd -n 3 zhangsan # 设置密码过期后2天内仍可登录,但会警告 passwd -i 2 zhangsan这些参数本质是在修改 /etc/shadow 中的第三、四、五等字段。好好利用它们,你就能把密码生命周期管理做得像企业级目录服务一样规整,而不需要额外安装任何管理软件。
4. 实战排雷:高频报错与排查实录
4.1 报错:"password for user is not changed"
这句报错常见于非交互模式。可能是因为新旧密码完全相同,也可能是 PAM 策略拒绝了这次修改,比如密码长度不足、包含用户名、连续字符过多等。处理思路:
- 先用
passwd -S 用户名查看当前状态和最后修改日期 - 检查 PAM 配置文件,通常位于 /etc/pam.d/passwd 和 /etc/security/pwquality.conf
- 确认新密码符合复杂度要求后再重试
我遇到的一个真实案例是某自动化任务连续一个月失败,就是因为密码词典中包含了用户名片段,被 pwquality 模块拦截。检查日志时才发现,系统完全没有报错,只是 passwd 返回了非零退出码,而脚本没有捕获这个退出码。
4.2 报错:"authentication token manipulation error"
这个报错通常发生在写入 /etc/shadow 阶段失败。可能的原因包括:
- 磁盘空间已满,导致文件写入失败
- /etc/shadow 文件被意外修改过权限或属主,passwd 无法写入
- 文件系统处于只读挂载状态
排查时先执行df -h确认磁盘空间,再看/etc/shadow权限(应当仅 root 可写),最后用mount | grep ' / '检查根分区挂载选项。这三个方向基本能覆盖绝大多数情况。
4.3 报错:"Cannot open file: /etc/shadow"
如果连 root 都遇到这个报错,多半是系统环境出了问题,比如容器内权限不足、SELinux 拦截、或者 shadow 文件被误删。在容器中运行某些基础镜像时,shadow 文件可能不存在或为空,需要先用pwconv生成对应的 shadow 条目。在物理机或虚拟机环境中,这种情况极少见,一旦出现建议立即检查文件完整性和系统日志。
4.4 排查工具与速查表
为了让大家遇到问题时少走弯路,我把常用的查看命令整理成一个速查表:
| 需求 | 命令 |
|---|---|
| 查看自己密码状态 | passwd -S |
| 查看指定用户密码状态(需 root) | sudo passwd -S 用户名 |
| 查看密码哈希算法 | awk -F: '/用户名/ {print $2}' /etc/shadow |
| 查看密码过期时间 | chage -l 用户名 |
| 锁定账户 | sudo passwd -l 用户名 |
| 解锁账户 | sudo passwd -u 用户名 |
chage -l是一个容易被忽略但极其有用的命令,它能以人类可读的格式展示"密码过期时间""最小间隔""最大间隔""告警天数"等信息,比直接看 shadow 文件的数字字段直观得多。我强烈建议排查密码策略问题时,优先用 chage 做第一轮分析。
5. 安全加固:让 passwd 用得再稳一点
5.1 密码强度与 PAM 配置
默认情况下,很多发行版的密码强度策略相对宽松。如果你管理的是一台生产服务器,建议检查 /etc/security/pwquality.conf 中的几个关键参数:
minlen = 12 dcredit = -1 ucredit = -1 lcredit = -1 ocredit = -1这几个参数的含义分别是:最小长度12位;新密码至少包含 1 个数字、1 个大写字母、1 个小写字母和 1 个其他字符。负数表示"至少需要多少个",正数表示"最多允许多少个"。把这些配置好后,即使员工自己用 passwd 改密,也会被强制要求遵守强度规则。
5.2 避免在命令行暴露明文密码
这是我最想强调的一点。网上能找到大量类似echo "password" | passwd --stdin user的示例,但不少人忽略了进程列表的安全问题。当你执行这样的命令时,虽然密码没有出现在命令行参数中,但管道字符本身不会把密码暴露到 /proc 里。相对安全。但如果用下面这种写法,密码就会暴露在进程列表中:
passwd --stdin zhangsan "PlainTextPassword"(注:这种用法本身就是错误的,passwd 并不接受密码作为位置参数,我只是用它来强调"不要把密码写在命令行参数里"的风险。)
脚本中用--stdin+ 重定向文件的方式,配合chmod 600限定文件权限,才是稳妥方案。
5.3 警惕弱哈希与旧算法
如果你手头维护的是老系统,或者手动生成的哈希使用了$1$这种 MD5 前缀,建议尽快规划升级。现代系统默认使用 SHA-512 或 yescrypt,抗暴力破解能力强很多。判断当前系统默认算法最简单的方法是修改一次密码后,查看/etc/shadow中哈希值的前缀。如果发现是$1$,说明系统配置可能还停留在旧时代,需要检查/etc/pam.d/system-auth或/etc/login.defs中的加密算法设定。
5.4 root 密码丢失的救急思路
如果你把 root 密码改了之后忘记了,又没有任何免密通道,只能走单用户模式或救援模式。不同发行版的处理方法略有差异,但核心思路一致:在引导过程中进入救援模式,把根分区挂载为可写,然后直接修改 shadow 文件,或使用 chroot 环境执行 passwd。这些操作需要物理或带外控制台访问,远程 SSH 是救不回来的。所以再次强调:改完密码立刻验证,别等会话断了才后悔。
6. 自动化场景中的进阶用法与思考
6.1 云主机批量初始化时的密码设置
在云主机或容器平台的初始化脚本中,经常需要为新用户设置初始密码。有些镜像自带 cloud-init,它会调用类似 chpasswd 的工具来处理用户密码。如果你需要在自己构建的镜像里做同样的操作,建议在脚本中显式调用 chpasswd,而不是 passwd --stdin,因为 chpasswd 在大多数发行版中都是标配,还能避免 passwd 对--stdin支持不一致的问题。
批量初始化时还有一个小技巧:先用openssl passwd -6生成哈希,再直接把哈希写入 /etc/shadow。这种方式更隐蔽,也不会触发 PAM 的交互流程:
HASH=$(openssl passwd -6 'MySecret123') useradd -m -p "$HASH" zhangsanopenssl passwd -6中的-6表示生成 SHA-512 哈希,生成的哈希值可以直接放到 useradd 的-p参数后面。这样做的好处是,密码不会出现在 shell 历史中,也不会被 passwd 命令的进程列表捕获。
6.2 密码轮换任务的容错设计
公司内部经常有"每90天强制改密"的合规要求,运维同学通常会用 cron 或定时任务配合 chpasswd 批量轮换密码。这类任务必须设计好容错:
- 生成密码时使用
openssl rand -base64 12,确保强度 - 记录每个用户改密前的状态和执行结果,改密失败的要自动告警
- 改密后立即用新密码执行一次 SSH 登录探测(如果允许的话)
- 把所有日志集中存储,避免改密后无法追溯
我见过一个翻车案例:脚本里用date +%s生成随机种子,导致所有用户的密码在一个特定时间段内完全相同。排查了一整天才发现,问题是随机种子可控。密码生成一定要用足够随机的方式,openssl rand是靠谱的,别自己拼字符串。
6.3 与集中身份认证系统的衔接
在引入 LDAP 或企业级身份认证系统之后,本地 passwd 命令的作用会弱化,但依然有存在的必要:至少 root 密码和紧急救援账户的密码还是要靠本地管理。两类密码体系并存时,一个重要原则是:本地账户作为应急通道,密码必须足够复杂且定期更新,不要与统一身份平台使用相同密码。否则一旦本地密码泄漏,攻击者就拿到了一个绕过统一认证的后门。
7. 我的实操心得与一些容易被忽略的细节
写到这里,该讲的机制和命令都讲得差不多了。最后分享几个我自己踩坑总结出来的小经验,不一定写在哪本手册里,但非常实用。
第一个心得是:改密后一定要看退出码。很多脚本里 pipeline 处理不当,passwd 执行失败了,但退出码被管道后面的命令覆盖,导致任务显示成功。正确做法是设置set -o pipefail,或者在执行后立刻检查$?。尤其是用 chpasswd 批量操作时,直接看退出码比看输出靠谱得多。
第二个心得是:尽量不要直接手动编辑 /etc/shadow。即使在救援模式下,我仍然优先用 chroot + passwd 而不是 vi 去改哈希字段。原因很简单:手抖改错一个字符,账户就废了,而找问题的时间远比执行一条命令要长。如果你是在应急状态下不得不手改,至少先备份一份 shadow 文件。
第三个心得是:密码策略要设,但别设死。有些企业把minlen改成 20 位,并启用所有复杂度要求,结果员工只能把密码写在便利贴上。密码策略的合理性需要平衡安全与可用性,12位以上加常规复杂度要求已经能挡住绝大多数暴力破解,过度限制只会催生"反模式"。
第四个心得是:关注 /etc/login.defs 里的全局默认值。你可以通过passwd -x给单个用户设置密码有效期,但如果希望全系统默认生效,就要改 /etc/login.defs 中的PASS_MAX_DAYS、PASS_MIN_DAYS、PASS_WARN_AGE。这个文件影响的是 useradd 新建用户时的默认值,对已有用户不生效,注意区分。
最后想说的是,passwd 看起来是个小命令,但它是 Linux 账户安全最重要的入口之一。把这个入口的机制理解透,再配合合理的策略和脚本容错,你的系统和你的睡眠质量都会好很多。遇到问题别急着删库重装,先看一眼 /etc/shadow 和日志,多半能找到答案。