Series · Linux · Chapter 5

Linux 用户管理

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

如果你只在自己一台笔记本上跑过 useraddpasswd,这篇文章里的大多数细节你可能根本用不上。可一旦同一台机器上同时坐着多个人、跑着多个服务,“用户管理"就不再是行政流程,而是安全模型本身:它决定了谁能登进来、进程以哪个 UID 写文件、sudo 把哪些命令提到 root、密码被偷之后还能被用多久。

这篇文章按一条主线把模型讲完。先看 /etc/passwd/etc/shadow 的原始结构——因为这一节里所有命令本质上都是在改这两个文件;再说用户和组之间最容易讲反的关系;接着是完整的生命周期命令(useraddusermodpasswdchageuserdel)、sudovisudo 的正确写法,最后落到那一层把所有东西串起来的 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——用户的数字 ID。内核眼里只有这个数,名字只是给人看的。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——认证通过后被 exec 起来的程序。把它设成 /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 里成员列表包含这个用户的那些行)。

一个用户、一个初始组、若干附加组

为什么要分成两类?

  • 初始组决定默认属组:alicetouch foo,文件就是 alice:alice(或者她的初始组是什么就是什么)。它也是登录后内核给进程设的 egid
  • 附加组是用来发额外权限的。把 alice 加进 docker,她就能跟 docker socket 说话;加进 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                            # 解锁

锁定的账户依然存在、依然拥有自己的文件,root 依然可以 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
# Last password change         : Apr 21, 2026
# Password expires             : Jul 20, 2026
# Password inactive            : Aug 19, 2026
# Account expires              : never
# Minimum number of days...    : 1
# Maximum number of days...    : 90
# Number of days of warning... : 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(注意:不会同时 chown 已有文件!)
sudo groupdel developers              # 删除(如果是某人的初始组会拒绝)
sudo gpasswd -a alice developers      # 把 alice 加进 developers
sudo gpasswd -d alice developers      # 把 alice 从 developers 移出
sudo gpasswd -A alice developers      # 让 alice 当组管理员

gpasswd 是专门管 /etc/group / /etc/gshadow 的封装,比通过 usermod -G 间接动这两个文件更安全;它还能把"管理某个组的成员"这件事下放给一个非 root 用户——这就是"组管理员"角色的用法。

sudo:把一条命令变成 root

直接用 root 登录有两个错。第一,rm -rf / 应该有阻力;第二,审计日志里只能看见"root 干了什么事",在不止一个人有 root 密码的环境里完全没用。sudo 把这两件事都修了:每次调用都按真实用户记账,而策略文件能让你只发出恰好够用的权限。

sudo 是怎么决定要不要执行你这条命令的

一条 sudoers 规则要在五个维度上同时匹配,命令才会执行:

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

照着读一遍:",在哪台主机上,可以以谁的身份,带什么标签,执行哪些命令。"

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 把它们包进来)。它们更适合走版本控制、走包管理、走代码评审,比单一巨大的文件好维护得多。

真正会用到的几种写法

# 完整 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,比手动读这些文件靠谱得多。

susudo 的差别

su -目标用户的密码开一个 shell(默认是 root);sudo你自己的密码以另一个用户身份跑一条命令。几乎所有场景下都该选 sudo:审计粒度更细,而且你永远不需要把 root 密码告诉别人。

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

sudosshdlogingdmcronsupasswdcrond——它们里面没有一个自己实现了密码校验。全部委托给 PAM(Pluggable Authentication Modules),一个由 .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——只在 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 <自己> 验证一下,再退出登录。

定位问题最有用的工具就是日志:

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 里的 AllowUsers / DenyUsers;密码已经过了 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 是 owner+group rwx,others 全无

2770 里那个 2目录上的 SGID 位:新建文件继承父目录的属组,而不是创建者的初始组。如果不加这位,alice(初始组 alice)建出来的文件会是 alice:alicebob 就改不动。

如果还想再细——比如"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 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 批量建用户

 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 "skip $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"     # 'last change = epoch' 的小技巧:首次登录必须重设
  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 unit 里的 User= / Group= / DynamicUser= 是怎么把"建一个服务账户"这件事变得几乎不必要的。

边上可以放着的参考:man 5 sudoersman 8 pam.dman 5 shadow,以及——如果哪天你需要给十几台以上的机器做集中身份认证——FreeIPA / SSSD 的官方文档。

Liked this piece?

Follow on GitHub for the next one — usually one a week.

GitHub