系列 · Linux · 第 5 篇

Linux(五):用户管理

把 Linux 账户从「会跑 useradd」升级到能讲清楚的层次:/etc/passwd 与 /etc/shadow 的字段含义与联动、初始组与附加组的区别、sudo 的判定流程、useradd/usermod/passwd/chage/userdel 的完整生命周期,以及底下那一层 PAM 是怎么把这些黏在一起的。

如果你只是在自己的笔记本上用过 useraddpasswd,那大概率不需要考虑太多细节。但只要一台机器上有多个用户或者运行了多个服务,“用户管理”就不再只是简单的操作,而是整个安全模型的核心:它决定了谁能登录系统、进程写入文件时归属哪个 UID、哪些命令可以通过 sudo 提权到 root,以及被窃取的密码还能被利用多久。

Linux(五):用户管理 — 章节概览图

本文会沿着一条主线完整讲解这个模型。首先从 /etc/passwd/etc/shadow 的原始结构入手——毕竟这一领域的所有命令本质上都是围绕这两个文件进行修改;然后详细说明用户和组之间的关系(这也是初学者最容易混淆的部分);接着介绍用户生命周期管理的相关命令(如 useraddusermodpasswdchageuserdel),以及如何正确配置 sudo 和使用 visudo;最后深入探讨将认证、账户策略、密码规则和会话管理串联起来的 PAM 框架。


心智模型:账户就是几个文本文件里的一行#

在开始讨论具体命令之前,先来了解一下数据模型。Linux 的用户账户信息存储在三个简单的文本文件中,每个文件一行代表一个实体,字段之间用冒号分隔。

  • /etc/passwd —— 公开信息。每一行对应一个账户(包括人类用户和服务账户)。所有人都能读取,但只有 root 用户可以修改。
  • /etc/shadow —— 敏感信息。每一行存储一个账户的密码哈希以及密码老化策略。只有 root 用户可以访问。
  • /etc/group —— 每行代表一个用户组,末尾是一个逗号分隔的成员列表。

还有 /etc/gshadow(用于存储组密码和组管理员信息,实际很少使用)以及 /etc/skel(新用户家目录的模板目录)。

/etc/passwd 一行的解剖以及它和 /etc/shadow 的关联

/etc/passwd 文件的七个字段从左到右依次是:

  1. username —— 登录名,在同一台主机上必须唯一。
  2. 密码占位符 —— 通常为 x,表示“实际的密码哈希存储在 /etc/shadow 中”。如果这里是一个 * 或空值,则表示该账户没有可用密码(这与“锁定”状态不同)。
  3. UID —— 用户的数字标识。内核只认这个数字,用户名只是为了方便人类识别。0 是超级用户 root,1–999 保留给系统服务,1000+ 分配给人类用户(CentOS 6 及更早版本从 500 开始分配人类用户)。
  4. GID —— 用户的主组 ID。用户创建的新文件默认属于这个组。
  5. GECOS —— 最初是 General Electric Comprehensive Operating System 的全名字段,如今用来存储自由格式的注释信息。可以通过 chfn 命令编辑。
  6. 家目录 —— 用户登录后的 $HOME 路径。如果在使用 useradd 时加上 -m 参数,系统会从 /etc/skel 模板目录复制内容到这里。
  7. 登录 shell —— 用户认证成功后执行的程序。将其设置为 /sbin/nologin/usr/sbin/nologin 可以禁止交互式登录(这是服务账户的标准配置)。

对应的 /etc/shadow 文件中的每一行包含以下信息:密码哈希(前缀表示算法——$6$ 表示 SHA‑512,较新的 Debian/Ubuntu 默认使用 $y$ 表示 yescrypt,$1$ 表示 MD5,现代系统上不应该再看到这种算法)、上次修改密码的天数(自 1970-01-01 起计算),以及五个与密码老化相关的字段:minmaxwarninactiveexpire。如果哈希值前有 !*,则表示账户被锁定——账户存在,但任何密码都无法匹配。

切勿直接用普通文本编辑器修改这些文件。推荐使用 vipwvigr,它们会正确地加锁(如 /etc/passwd.lock/etc/shadow.lock),避免在保存时与其他进程(如 useradd)冲突导致文件损坏。更好的做法是使用封装好的高级命令。

用户、初始组、附加组#

这是许多教程容易混淆的地方。每个用户账户都有且仅有一个主组(记录在 /etc/passwd 文件中的 GID),以及零个或多个附加组(记录在 /etc/group 文件中,成员列表包含该用户的那些行)。

用户属于一个主组和多个附加组

为什么要区分这两种组呢?

  • 主组决定了文件的默认属组。比如,当用户 alice 执行 touch foo 创建文件时,文件的属组会是她的主组(例如 alice:alice)。此外,主组还决定了用户登录时内核为进程设置的 egid
  • 附加组则用来赋予额外权限。如果把 alice 添加到 docker 组,她就能访问 Docker 的套接字;如果加入 sudo(Debian 系统)或 wheel(RHEL 系统),她就能获得提权能力。但这些操作不会影响她创建文件时的默认属组。

一个常见的误解是:如果 alice 的主组名就是 alice,那么在 /etc/group 文件中 alice 对应的那一行里,你不会看到她的名字。这是因为主组的归属信息存储在 /etc/passwd 中,而不是 /etc/group/etc/group 的成员列表只记录附加组成员。如果想查看完整的组信息(包括主组和附加组),可以直接询问内核:

1
2
3
4
id alice
# uid=1001(alice) gid=1001(alice) groups=1001(alice),998(docker),27(sudo)
groups alice
# alice : alice docker sudo

生命周期命令#

每个账户都会经历相同的五个阶段。每条命令只修改特定的文件子集,只要清楚每条命令的作用范围,恢复和审计就变得非常简单。

Linux 账户的生命周期:useradd, usermod, passwd, 锁定, userdel

useradd —— 创建账户#

1
2
sudo useradd -m -s /bin/bash -c "Alice Wang" alice
sudo passwd alice

以下是一些值得记住的关键参数:

参数作用
-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 字段(注释字段),通常用来描述用户信息。

两种常见用法示例:

1
2
3
4
5
6
# 创建一个人类用户,加入 developers 和 docker 组:
sudo useradd -m -s /bin/bash -G developers,docker bob
sudo passwd bob

# 创建一个运行 nginx 的服务账户:
sudo useradd -r -s /sbin/nologin -d /var/lib/nginx -M nginx

-M 表示不创建家目录,因为很多服务账户的状态数据存放在其他地方(如 /var/lib/<service>),并不需要单独的家目录。

usermod —— 修改现有账户#

使用 -G 参数时需要注意,它会完全替换现有的附加组列表。如果想追加组,必须使用 -aG

1
2
3
4
5
# 正确:将 alice 添加到 sudo 组,同时保留其他附加组。
sudo usermod -aG sudo alice

# 错误:这会将 alice 的附加组直接覆盖为 {sudo}。
sudo usermod -G sudo alice

其他常用操作示例:

1
2
3
4
5
sudo usermod -s /bin/zsh alice                   # 修改登录 shell
sudo usermod -d /data/alice -m alice             # 迁移家目录并同步内容
sudo usermod -l awang -d /home/awang -m alice    # 修改用户名及家目录
sudo usermod -L alice                            # 锁定账户(在密码哈希前加 '!')
sudo usermod -U alice                            # 解锁账户

锁定的账户仍然存在,文件归属也不会改变,管理员仍可以通过 su - alice 切换到该账户——只是无法通过密码认证登录。这种操作非常适合处理员工离职但其文件仍需保留一段时间的情况。

passwdchage —— 密码管理与过期策略#

passwd 用于设置密码,而 chage 则用于定义密码的过期策略。

1
2
3
4
5
6
passwd                       # 修改自己的密码
sudo passwd alice            # 修改 alice 的密码
sudo passwd -l alice         # 锁定账户(效果等同于 `usermod -L`)
sudo passwd -u alice         # 解锁账户
sudo passwd -e alice         # 强制过期:alice 下次登录时必须重置密码
sudo passwd -d alice         # 删除密码(允许无密码登录,通常不推荐)

chage -l 查看当前策略,chage 修改策略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sudo chage -l alice
# 最后一次密码修改时间     : Apr 21, 2026
# 密码过期时间             : Jul 20, 2026
# 密码失效时间             : Aug 19, 2026
# 账户过期时间             : never
# 最小密码使用天数         : 1
# 最大密码使用天数         : 90
# 提前警告天数             : 7

# 一个合理的策略:每 90 天强制更换密码,提前 7 天提醒,
# 密码过期后 30 天禁用账户。
sudo chage -m 1 -M 90 -W 7 -I 30 alice

# 设置账户在特定日期过期(例如外包人员合同到期):
sudo chage -E 2026-12-31 alice

需要注意的是,“最大密码使用期限”并不是许多人想象中的安全控制手段。根据 NIST SP 800-63B 的现代建议,不推荐对人类账户强制周期性更换密码,因为这往往会导致用户选择可预测的模式。这种策略更适合服务账户或合规性要求明确的场景。对于人类账户,应更多依赖多因素认证、密码长度和泄露检测机制。

userdel —— 删除账户的最佳实践#

1
2
sudo userdel alice           # 删除账户,保留 $HOME
sudo userdel -r alice        # 删除账户,同时删除 $HOME 和邮件假脱机

潜在风险:如果 alice$HOME 之外还拥有文件,这些文件在账户删除后会变成“无主文件”,仅显示为一个裸 UID。如果后续创建的新账户分配了相同的 UID,这些文件会被新账户默默继承。避免这种情况需要依靠流程而非技术手段:

  1. 使用 usermod -L alice(或 passwd -l alice)立即锁定账户,禁止进一步登录。
  2. 等待:让 cron 任务完成,活动的 shell 关闭,并完成文件归属的审计。
  3. 使用 find / -uid $(id -u alice) -print 检查 $HOME 之外的文件归属,通过 chown 转移所有权或归档保存。
  4. 最后执行 userdel -r alice 完全删除账户及其相关文件。

组管理#

1
2
3
4
5
6
7
8
sudo groupadd developers              # 创建一个新组
sudo groupadd -g 2000 developers      # 创建一个新组并指定 GID
sudo groupmod -n devs developers      # 修改组名
sudo groupmod -g 3000 developers      # 修改组的 GID(注意:不会自动更改已有文件的所有权!)
sudo groupdel developers              # 删除组(如果该组是某个用户的主组,则拒绝删除)
sudo gpasswd -a alice developers      # 将用户 alice 添加到 developers 组
sudo gpasswd -d alice developers      # 将用户 alice 从 developers 组中移除
sudo gpasswd -A alice developers      # 设置 alice 为 developers 组的管理员

gpasswd 是专门用来管理 /etc/group/etc/gshadow 文件的工具,相比直接用 usermod -G 操作这些文件更加安全可靠。此外,它还支持将组成员管理权限委派给非 root 用户,这就是所谓的“组管理员”功能的实际应用场景。

sudo:把一条命令变成 root#

直接以 root 用户登录系统是不推荐的,原因有二。首先,像 rm -rf / 这样的危险操作理应受到限制;其次,审计日志只会记录“root 做了某些事”,而在多人共享 root 密码的情况下,这种记录毫无意义。sudo 解决了这两个问题:每次执行命令时都会记录实际用户的身份,同时通过策略文件精确分配权限,不多不少刚刚好。

sudo 是如何决定是否允许执行你的命令的

sudoers 文件中,每条规则需要在五个维度上匹配成功后,命令才能被执行:

1
user_or_%group  host=(runas_user:runas_group)  TAG=  command_list

可以这样理解:“,在哪台机器上,能以谁的身份,带什么标签,执行哪些命令。”

编辑 sudoers 文件?用 visudo,别手贱#

1
2
sudo visudo                       # 编辑主配置文件 /etc/sudoers
sudo visudo -f /etc/sudoers.d/ops # 编辑某个 drop-in 文件

visudo 是专门用来编辑 sudoers 文件的工具,它会在保存时进行语法检查,确保配置文件的合法性。如果语法有问题,它会拒绝写入。而直接用 vim 修改 /etc/sudoers 是运维事故的经典案例之一——一旦配置出错,sudo 自己都无法解析配置文件,没有任何方法能修复,除非进入单用户模式。

更推荐使用 /etc/sudoers.d/ 目录下的 drop-in 文件(主配置文件中有一行 @includedir 指令加载这些文件)。相比单一的庞大文件,drop-in 文件更适合版本控制、打包管理和代码审查。

实际工作中常用的几种配置方式#

# 完全的 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 不仅无效,还可能带来安全隐患,因为用户可以通过修改 $PATHnginx 指向其他程序。
  • 如果不指定参数,命令会按前缀匹配。比如,/usr/bin/systemctl restart nginx 只允许这条具体命令,而 /usr/bin/systemctl 则允许 systemctl 的所有功能(包括 poweroff)。
  • NOPASSWD: 虽然方便,但也危险。建议仅用于非交互式自动化任务;对于人类用户,还是应该要求输入密码。
  • Defaults requiretty(某些 RHEL 系统默认启用)会导致 sudo 在非 tty 环境下无法工作。为自动化账户单独添加一行 Defaults:bot !requiretty 即可解决。

sudo 启动时读取的文件顺序#

sudo 按以下顺序加载配置:先读取 /etc/sudoers,然后按字母顺序加载 /etc/sudoers.d/ 下的所有文件,接着解析用户组关系,最后应用 Defaults 配置。要查看某个用户的完整生效策略,可以用 sudo -ll,比手动分析配置文件更可靠。

susudo 的区别#

su - 会以目标用户的身份启动一个 shell(默认是 root),需要输入目标用户的密码;而 sudo 则是以另一个用户的身份运行某条命令,只需要输入你自己的密码。绝大多数情况下,sudo 是更好的选择:它的审计记录更详细,而且你永远不需要共享 root 密码。

PAM:所有这些东西底下的那一层#

无论是 sudosshdlogin,还是 gdmcronsupasswdcrond,这些工具都不会自己去校验密码,而是把这项任务交给 PAM(Pluggable Authentication Modules)。PAM 是一组动态链接库(.so 文件),每个服务的配置都存放在 /etc/pam.d/ 目录下。理解 PAM 的工作机制,可以让你从“不知道为什么这个账号登不进去”的迷茫,迅速转变为五秒钟定位问题的高手。

PAM 处理登录请求的流程:auth, account, password, session

一个 PAM 服务配置文件通常包含四个模块栈:

  • auth —— 验证身份。pam_unix.so 检查 /etc/shadowpam_sss.so 负责与 SSSD/LDAP/AD 交互;pam_google_authenticator.so 提供 TOTP 双因素认证。
  • account —— 即使密码正确,也需要判断账户是否允许当前登录。比如密码老化策略、nologin 标志、时间段限制、来源主机检查等。
  • password —— 仅在修改密码时触发。pam_pwquality.so 检查密码强度,pam_pwhistory.so 防止重复使用旧密码,最后由 pam_unix.so 写入新的密码哈希。
  • session —— 登录成功后设置用户的工作环境。例如通过 pam_limits.so 设置资源限制,pam_systemd.so 创建 systemd 用户会话,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 <自己> 测试一下,确认无误后再退出登录。

排查问题时,日志是最有力的工具:

1
2
3
4
sudo journalctl -u sshd -e            # 查看 sshd 的日志
sudo journalctl _COMM=sudo --since "1h ago"
sudo grep "alice" /var/log/auth.log   # Debian 系统
sudo grep "alice" /var/log/secure     # RHEL 系统

如果密码明明正确却仍然无法登录,通常是因为以下原因之一:账户被锁定(/etc/shadow 中密码哈希前有 !);shell 被设置为 /sbin/nologin/etc/nologin 文件存在导致 pam_nologin 阻止所有登录;sshd_config 中配置了 AllowUsersDenyUsers;或者密码已经超过了 inactive 时间限制。

几个现场常见的方案#

共享项目目录的配置#

目标:/srv/projectdevelopers 组的所有成员可读写,对组外用户完全不可见。同时,确保在该目录下创建的新文件始终归属于 developers 组,避免同事之间因权限问题互相“锁死”。

1
2
3
4
5
6
7
sudo groupadd -r developers
sudo usermod -aG developers alice
sudo usermod -aG developers bob

sudo mkdir -p /srv/project
sudo chown root:developers /srv/project
sudo chmod 2770 /srv/project   # 2 表示 SGID;770 表示属主和属组有 rwx 权限,其他用户无权限

这里的 2770 中的 2目录的 SGID 位,它的作用是让新创建的文件继承目录的属组,而不是创建者的默认组。如果没有设置 SGID,当 alice(默认组为 alice)创建文件时,文件的属组会是 alice,导致 bob 无法编辑。

如果需要更精细的控制,比如“developers 组可写,但 carol 只读”,可以使用 POSIX ACL:

1
2
3
4
sudo setfacl -m g:developers:rwx /srv/project
sudo setfacl -m u:carol:r-x /srv/project
sudo setfacl -d -m g:developers:rwx /srv/project   # 默认 ACL,适用于未来创建的文件
getfacl /srv/project

正确配置服务账户#

1
2
3
4
5
6
7
sudo useradd --system \
             --home-dir /var/lib/myapp \
             --shell /usr/sbin/nologin \
             --no-create-home \
             --user-group \
             myapp
sudo install -d -o myapp -g myapp -m 0750 /var/lib/myapp /var/log/myapp

--system 选项会分配一个低于普通用户的 UID,并跳过密码老化设置。--user-group 会自动创建一个与用户名同名的组。--no-create-home 的原因是,我们希望通过 install 手动创建状态目录,并将权限固定为 0750。在 systemd 配置中,服务应以 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 文件批量创建用户#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/usr/bin/env bash
set -euo pipefail
while IFS=, read -r username fullname groups; do
  id "$username" &>/dev/null && { echo "跳过 $username(已存在)"; continue; }
  sudo useradd -m -s /bin/bash -c "$fullname" -G "$groups" "$username"
  # 生成一次性随机密码,并强制用户首次登录时修改密码
  pw=$(openssl rand -base64 16)
  echo "$username:$pw" | sudo chpasswd
  sudo chage -d 0 "$username"     # 设置 '上次密码修改时间为纪元时间',强制首次登录重置密码
  echo "$username,$pw" >> /root/initial-passwords.csv
done < users.csv

chpasswd 是处理批量更新密码的正确工具,它接受 user:password 格式的输入;而 passwd --stdin 仅适用于 RHEL 系统。chage -d 0 是一个小技巧,可以在不分配长期有效随机密码的情况下,强制用户首次登录时更改密码。

下一步#

本文已经完整介绍了操作模型的核心内容:文件结构、生命周期相关的命令、sudo 策略语言,以及 PAM 栈。接下来的两篇文章会在此基础上进一步展开:

  • Linux 文件权限 —— 深入讲解 rwx 权限、前面提到的共享目录用到的 SUID/SGID 位,以及 POSIX ACL 的使用。
  • Linux 系统服务管理 —— 探讨 systemd 配置单元中 User=Group=DynamicUser= 指令如何让传统的服务账户几乎不再必要。

还有一些值得参考的手册页和文档:man 5 sudoersman 8 pam.dman 5 shadow。如果你需要为多台主机(比如十几台以上)实现集中身份管理,还可以查阅 FreeIPA 和 SSSD 的官方文档。

本系列

Linux 9 篇

  1. 01 Linux(一):使用基础
  2. 02 Linux(二):文件权限 —— rwx、chmod、chown 与超越它们的机制
  3. 03 Linux(三):磁盘管理
  4. 04 Linux(四):软件包管理
  5. 05 Linux(五):用户管理 当前
  6. 06 Linux(六):系统服务管理
  7. 07 Linux(七):进程与资源管理 —— 从 top 到 cgroups
  8. 08 Linux(八):文件操作深入解析
  9. 09 Linux(九):Vim 编辑器精要

读有所得?

GitHub 关注我 → 新文周更

GitHub