Linux 用户管理
把 Linux 账户从「会跑 useradd」升级到能讲清楚的层次:/etc/passwd 与 /etc/shadow 的字段含义与联动、初始组与附加组的区别、sudo 的判定流程、useradd/usermod/passwd/chage/userdel 的完整生命周期,以及底下那一层 PAM 是怎么把这些黏在一起的。
如果你只在自己一台笔记本上跑过 useradd 和 passwd,这篇文章里的大多数细节你可能根本用不上。可一旦同一台机器上同时坐着多个人、跑着多个服务,“用户管理"就不再是行政流程,而是安全模型本身:它决定了谁能登进来、进程以哪个 UID 写文件、sudo 把哪些命令提到 root、密码被偷之后还能被用多久。
这篇文章按一条主线把模型讲完。先看 /etc/passwd 和 /etc/shadow 的原始结构——因为这一节里所有命令本质上都是在改这两个文件;再说用户和组之间最容易讲反的关系;接着是完整的生命周期命令(useradd、usermod、passwd、chage、userdel)、sudo 与 visudo 的正确写法,最后落到那一层把所有东西串起来的 PAM 框架。
心智模型:账户就是几个文本文件里的一行
先讲数据模型,再讲命令。Linux 的账户信息存在三个扁平的文本文件里,一行一条记录,字段用冒号分隔:
/etc/passwd——公开。每行一个账户(人类账户和服务账户都在这里)。所有人可读,只有 root 可写。/etc/shadow——机密。每行一个账户,存密码哈希和老化策略。只有 root 可读。/etc/group——每行一个组,最后一段是逗号分隔的成员列表。
另外还有 /etc/gshadow(组密码和组管理员,实际很少用)和 /etc/skel(每个新家目录的模板)。

/etc/passwd 的七个字段,从左到右:
- username——登录名,本机唯一。
- 密码占位符——几乎永远是
x,意思是"真正的哈希在/etc/shadow里”。如果这位是*或者空,意味着没有可用密码(这跟"被锁定"是两回事)。 - UID——用户的数字 ID。内核眼里只有这个数,名字只是给人看的。
0是 root,1–999留给系统服务,1000+是人类账户(CentOS 6 及更早从500起算)。 - GID——初始组的 ID。这个用户新建的文件,默认属组就是它。
- GECOS——历史上是 General Electric Comprehensive Operating System 的全名字段,今天就是个自由格式的注释。
chfn可以改它。 - 家目录——登录后的
$HOME。如果你给useradd加了-m,系统会从/etc/skel复制一份过来。 - 登录 shell——认证通过后被
exec起来的程序。把它设成/sbin/nologin或/usr/sbin/nologin就能让账户变成不可交互登录的状态(这是服务账户该有的样子)。
对应的 /etc/shadow 一行里则有:哈希本身(前缀决定算法——$6$ 是 SHA‑512,新版 Debian/Ubuntu 默认的 $y$ 是 yescrypt,$1$ 是 MD5,现代系统上不该再见到它)、上次改密码的天数(自 1970-01-01 起算),以及五个老化字段:min、max、warn、inactive、expire。哈希前面带 ! 或 * 表示"已锁定"——账户存在,但任何密码都对不上。
不要直接用普通编辑器去改这些文件。要改的话用
vipw和vigr,它们会拿正确的锁(/etc/passwd.lock、/etc/shadow.lock),避免你保存的瞬间正好和useradd撞车把文件写坏。更好的做法是用上层封装好的命令。
用户、初始组、附加组
这一节是教程里最容易讲反的地方。每个账户都恰好有一个初始组(/etc/passwd 里那个 GID),以及零个或多个附加组(/etc/group 里成员列表包含这个用户的那些行)。

为什么要分成两类?
- 初始组决定默认属组:
alice跑touch foo,文件就是alice:alice(或者她的初始组是什么就是什么)。它也是登录后内核给进程设的egid。 - 附加组是用来发额外权限的。把
alice加进docker,她就能跟 docker socket 说话;加进sudo(Debian)或wheel(RHEL),她就能提权。这些都不会改变她新建文件的默认属组。
一个常见的迷惑点:如果 alice 的初始组就叫 alice,你在 /etc/group 里那一行 alice 的成员列表里看不到她的名字——初始组的归属信息存在 /etc/passwd 里,不在 /etc/group 里。/etc/group 的成员列表只列附加成员。要看真正的并集,问内核:
| |
生命周期命令
每个账户都会经历同样的五个阶段。每条命令各自只动其中几个文件;只要你清楚谁动了谁,恢复和审计就都不是问题。

useradd——创建
| |
值得记住的几个参数:
| 参数 | 作用 |
|---|---|
-m | 创建家目录(同时把 /etc/skel 的内容复制进去)。不加 -m 的话,账户的 $HOME 会指向一个不存在的路径。 |
-s SHELL | 登录 shell:/bin/bash、/bin/zsh,或者服务账户用 /sbin/nologin。 |
-g GROUP | 初始组。默认行为是新建一个同名组、用同样的 GID(这就是"用户私有组"模型,所有现代发行版的标准布局)。 |
-G g1,g2 | 附加组,逗号分隔。逗号之间不要有空格。 |
-u UID | 指定 UID。在多台机器共享 NFS 的场景下用得上,可以保证文件归属一致。 |
-r | 创建系统用户(UID 落在系统区间,不参与老化,不建 /home)。 |
-c "..." | GECOS / 注释字段。 |
两种最常见的写法:
| |
-M 的意思是"不要建家目录"——很多服务账户不需要家目录,因为它们的状态目录在别的地方(一般是 /var/lib/<service>)。
usermod——原地修改
-G 的坑是它会整体替换已有的附加组列表。要追加,用 -aG:
| |
其他常用写法:
| |
锁定的账户依然存在、依然拥有自己的文件,root 依然可以 su - alice 切过去——只是任何密码都不会被认可。这正是员工离职、但他名下的文件可能还要再用几周时的标准做法。
passwd 和 chage——密码本身和它的过期策略
passwd 设密码;chage 设决定密码什么时候必须换的策略。
| |
chage -l 看当前策略;chage 改它:
| |
值得提醒的是,“最长使用期限"本身并不是大多数人想象中的安全控制——NIST SP 800-63B 这一代的指导意见,其实是反对强制周期性轮换人类密码的,因为这只会逼用户用可预测的套路。要用就用在服务账户和合规明确要求的场景上;对人类账户,靠多因素、长度、和泄露检测才是正路。
userdel——以及为什么应该先锁后删
| |
风险点:alice 在 $HOME 之外拥有的文件,删账户之后会变成"无主文件”,属主只剩一个光秃秃的 UID。下一次创建的账户如果分到了同一个 UID,会默默继承这些文件。要避免这件事,靠流程,不是靠技术:
usermod -L alice(或passwd -l alice)——立刻锁定,禁止再登录。- 等。等 cron 跑完,等还开着的 shell 关掉,等文件归属审计跑完。
find / -uid $(id -u alice) -print看看她在$HOME之外还拥有什么。chown转给别人,或者归档。- 这之后才是
userdel -r alice。
组管理
| |
gpasswd 是专门管 /etc/group / /etc/gshadow 的封装,比通过 usermod -G 间接动这两个文件更安全;它还能把"管理某个组的成员"这件事下放给一个非 root 用户——这就是"组管理员"角色的用法。
sudo:把一条命令变成 root
直接用 root 登录有两个错。第一,rm -rf / 应该有阻力;第二,审计日志里只能看见"root 干了什么事",在不止一个人有 root 密码的环境里完全没用。sudo 把这两件事都修了:每次调用都按真实用户记账,而策略文件能让你只发出恰好够用的权限。

一条 sudoers 规则要在五个维度上同时匹配,命令才会执行:
user_or_%group host=(runas_user:runas_group) TAG= command_list
照着读一遍:"谁,在哪台主机上,可以以谁的身份,带什么标签,执行哪些命令。"
用 visudo,永远不要直接编辑
| |
visudo 会拿锁、保存时跑 sudoers 语法校验,校验不过它直接拒绝写入。直接 vim /etc/sudoers 是少数几个能把一台机器彻底锁死、除了进单用户模式没别的办法救的事故来源——因为 sudo 自己读不懂自己的配置时,没有任何东西能帮你修。
更推荐用 /etc/sudoers.d/ 下面的 drop-in 文件(主文件里有一行 @includedir 把它们包进来)。它们更适合走版本控制、走包管理、走代码评审,比单一巨大的文件好维护得多。
真正会用到的几种写法
# 完整 root 权限,要密码。
alice ALL=(ALL:ALL) ALL
# 按组发:wheel(RHEL)或 sudo(Debian)的成员都能成 root。
%wheel ALL=(ALL:ALL) ALL
# 收敛:bob 只能重启 nginx,不要密码。
bob ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl status nginx
# 用别名让大策略读起来还像人话。
Cmnd_Alias NGINX_CTL = /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl reload nginx, \
/usr/bin/systemctl status nginx
User_Alias ONCALL = alice, bob, carol
ONCALL ALL=(root) NOPASSWD: NGINX_CTL
几个不那么直观的规则:
- 命令列表必须写绝对路径。
bob ... NOPASSWD: nginx没什么用,甚至可能开后门——用户可以让nginx在自己的$PATH上指向别的东西。 - 不指定参数的话,命令是按前缀匹配的。
/usr/bin/systemctl restart nginx只允许这一条;/usr/bin/systemctl等价于"systemctl 能干的它都能干"(包括poweroff)。 NOPASSWD:方便也危险。留给非交互的自动化用;人类账户该问就问。Defaults requiretty(RHEL 上偶尔会带)会让sudo在非 tty 的通道上直接挂掉。给自动化账户单独写一行Defaults:bot !requiretty。
sudo 启动时读什么
按顺序:先 /etc/sudoers,再 /etc/sudoers.d/ 下面所有文件按字母序,然后解析组成员关系,最后应用 Defaults。要看某个用户最终生效的策略,用 sudo -ll,比手动读这些文件靠谱得多。
su 和 sudo 的差别
su - 用目标用户的密码开一个 shell(默认是 root);sudo 用你自己的密码以另一个用户身份跑一条命令。几乎所有场景下都该选 sudo:审计粒度更细,而且你永远不需要把 root 密码告诉别人。
PAM:所有这些东西底下的那一层
sudo、sshd、login、gdm、cron、su、passwd、crond——它们里面没有一个自己实现了密码校验。全部委托给 PAM(Pluggable Authentication Modules),一个由 .so 库堆起来的栈,每个服务都在 /etc/pam.d/ 里有自己的配置。看懂 PAM 是把"我也不知道这个账户为什么登不上"变成五秒钟就能定位的关键。

一个 PAM 服务文件里最多有四个栈:
- auth——证明你是你。
pam_unix.so查/etc/shadow;pam_sss.so跟 SSSD/LDAP/AD 对接;pam_google_authenticator.so接 TOTP。 - account——就算密码对了,这个账户现在可以登吗?老化策略、
nologin标志、当前时间、来源主机都在这一层判断。 - password——只在
passwd触发时跑。强度规则(pam_pwquality.so)、复用规则(pam_pwhistory.so),最后pam_unix.so把新哈希写下去。 - session——成功登录之后给用户搭好工作环境:rlimit(
pam_limits.so)、systemd 用户 slice(pam_systemd.so)、首次登录时建家目录(pam_mkhomedir.so)、记lastlog。
每一行都带一个控制标志,决定它的结果怎么和栈里其他模块组合:
required——必须成功;失败的话整栈失败,但 PAM 会把剩下的模块跑完,让用户看不出具体哪一行挂了(这是故意的——告诉攻击者会泄露信息)。requisite——必须成功;失败立刻终止,不再往下跑。sufficient——只要它通过、并且前面没有required失败过,整栈直接通过。optional——结果会被忽略,除非它是栈里唯一的模块。
一个实际例子——在 Debian 上打开强密码策略。改 /etc/pam.d/common-password:
password requisite pam_pwquality.so retry=3 minlen=12 \
dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1 \
difok=4 enforce_for_root
password required pam_pwhistory.so remember=5 use_authtok
password [success=1 default=ignore] pam_unix.so obscure use_authtok yescrypt
它说的是:长度至少 12;至少各有一个数字 / 大写 / 特殊字符 / 小写(-1 的意思是"这一类至少给一分");和旧密码相同的字符不能超过 4 个;记住最近 5 次的哈希、不准复用;最后用现代的 yescrypt 写下去。改之前先用一个非 root 账户跑 passwd <自己> 验证一下,再退出登录。
定位问题最有用的工具就是日志:
| |
如果密码确实对,但还是登不上,几乎一定是这几种之一:账户被锁(/etc/shadow 里哈希前有 !);shell 设成了 /sbin/nologin;/etc/nologin 文件存在导致 pam_nologin 把所有人挡在门外;sshd_config 里的 AllowUsers / DenyUsers;密码已经过了 inactive 期。
几个现场常见的方案
共享项目目录
需求:/srv/project 对 developers 组成员可读写,对其他人完全不可见,并且组员在里面新建的文件自动保持 developers 属组——避免互相不小心把对方锁在外面。
| |
2770 里那个 2 是目录上的 SGID 位:新建文件继承父目录的属组,而不是创建者的初始组。如果不加这位,alice(初始组 alice)建出来的文件会是 alice:alice,bob 就改不动。
如果还想再细——比如"developers 可写,但 carol 只读"——上 POSIX ACL:
| |
服务账户的标准写法
| |
--system 拿一个低段 UID,跳过老化。--user-group 顺手建一个同名组。--no-create-home 是因为状态目录我们要用 install 自己建,把权限位钉到 0750。systemd unit 里写 User=myapp Group=myapp,这就是磁盘上唯一应该拥有这个应用数据的身份。
分级 sudo
Cmnd_Alias READ_LOGS = /usr/bin/journalctl, /usr/bin/tail, /usr/bin/less
Cmnd_Alias NGINX_CTL = /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl reload nginx, \
/usr/bin/systemctl status nginx
# 完整管理员
alice ALL=(ALL:ALL) ALL
# 值班:只能动 nginx,不要密码(凌晨三点被叫起来,没空在键盘上跟密码较劲)
bob ALL=(root) NOPASSWD: NGINX_CTL
# 支持:读日志,但要密码
carol ALL=(root) READ_LOGS
从 CSV 批量建用户
| |
chpasswd 接受 user:password 行,是批量改密的正确工具;passwd --stdin 只在 RHEL 系上有。chage -d 0 是个特别好用的小技巧,可以在不发非过期随机密码的前提下,强制用户首次登录就改一次。
接下来读什么
这一章把操作模型讲完了:文件结构、生命周期命令、sudo 策略语言、PAM 栈。系列里下面两篇会直接接着这套基础往下走:
- Linux 文件权限
——
rwx、上面共享目录里用到的 SUID / SGID、以及 POSIX ACL。 - Linux 系统服务管理
——systemd unit 里的
User=/Group=/DynamicUser=是怎么把"建一个服务账户"这件事变得几乎不必要的。
边上可以放着的参考:man 5 sudoers、man 8 pam.d、man 5 shadow,以及——如果哪天你需要给十几台以上的机器做集中身份认证——FreeIPA / SSSD 的官方文档。