Appearance
04|用户与组
第 1 讲登录机器后敲 whoami,看到自己是 root——root 权限最大,敲错命令可能删掉整个系统,生产环境里不该这么用。那该用什么身份?这就涉及"用户"的概念。
再往后会碰到更多和用户有关的问题:访问某个文件被拒"permission denied"、想用 docker 命令提示没权限、新同事要登录服务器得给他开个账号、服务进程该以什么身份跑。这些都绕不开"用户与组"。
本篇讲清 Linux 里用户和组是怎么回事、怎么管账号、su 和 sudo 有什么区别。
一、UID 与 GID
Linux 内核做权限判断时,识别的是数字 ID,而不是用户名。用户名只是给人查看的标签,内核认的是 UID 和 GID。理解这一点,能解释后续的许多现象。
| 名称 | 含义 |
|---|---|
| UID | 用户 ID。0 是 root,拥有完整权限 |
| GID | 主组 ID。每个用户必须属于且只属于一个主组 |
| 附加组 | 用户额外加入的其他组,一个用户可以同时属于多个附加组 |
查看当前用户身份:
bash
$ whoami
deploy
$ id
uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),10(wheel),996(docker)
$ groups
deploy wheel docker排查权限问题时,id 的输出比 whoami 更有价值。某账号是否能访问某文件,取决于它的 UID 和所有 GID(包括附加组),仅看用户名无法判断完整权限范围。例如 whoami 显示用户是 deploy,而 id 显示该用户属于 docker 组——这意味着该用户可以无 sudo 直接使用 docker 命令(因为 docker socket 通常对 docker 组开放),这一权限仅从用户名是看不出来的。
UID 在跨机器复制时的陷阱
文件系统中记录的 owner 和 group 实际上是 UID 与 GID 的数字,不是字符串。如果两台机器上同一个 UID 对应的用户名不同,文件复制后会出现"属主名称变了"的现象——本质上 UID 没变,变的是 UID 到用户名的映射:
bash
# 源机器(UID 1001 是 nginx)
[server-a]$ ls -l /tmp/app.log
-rw-r--r-- 1 nginx nginx 1024 Jun 23 10:30 /tmp/app.log
# 目标机器(UID 1001 是 deploy)
[server-b]$ scp server-a:/tmp/app.log /tmp/
[server-b]$ ls -l /tmp/app.log
-rw-r--r-- 1 deploy deploy 1024 Jun 23 10:30 /tmp/app.log在分布式系统、共享存储、容器化部署的场景下,这种 UID 映射不一致经常导致权限混乱。一种常见做法是为关键服务账号(如 nginx、mysql)在所有机器上保留相同的 UID,通过 useradd -u 显式指定 UID 值。
二、用户与组的关键文件
Linux 用户与组的信息存储在几个固定文件中:
| 文件 | 内容 | 权限 |
|---|---|---|
/etc/passwd | 用户名、UID、GID、家目录、Shell | 全员可读 |
/etc/shadow | 密码哈希、密码过期策略 | 仅 root 可读 |
/etc/group | 组名、GID、组成员列表 | 全员可读 |
/etc/gshadow | 组密码(基本不使用) | 仅 root 可读 |
查询用户与组信息时,优先使用 getent 而非 cat。getent 会走系统的名称服务交换(NSS)机制,在 LDAP、NIS 等集中式认证环境下也能查到外部账号;而 cat /etc/passwd 仅能看到本机文件中的本地账号。
bash
getent passwd nginx # 查询 nginx 用户信息
getent group wheel # 查询 wheel 组信息及成员
getent passwd | wc -l # 统计所有可用账号(包括 LDAP/NIS 等外部源)/etc/passwd 一行的结构
以 root 用户为例:
root:x:0:0:root:/root:/bin/bash字段之间用冒号分隔,7 列含义:
| 字段 | 值 | 说明 |
|---|---|---|
| 用户名 | root | 登录时使用的名称 |
| 密码占位符 | x | 真实哈希在 /etc/shadow 中,此处仅占位 |
| UID | 0 | 用户 ID |
| GID | 0 | 主组 ID |
| GECOS | root | 注释字段,通常存放全名或说明 |
| 家目录 | /root | 登录后的初始工作目录 |
| Shell | /bin/bash | 登录后启动的 Shell 程序 |
服务账号的 Shell 字段通常设置为 /sbin/nologin 或 /usr/sbin/nologin,表示该账号不能交互登录,但服务进程仍可以该账号身份运行。
三、创建用户
最简单的创建命令:
bash
useradd app
passwd app但 useradd 的默认行为依赖于 /etc/login.defs 和 /etc/default/useradd 这两个配置文件——是否自动创建家目录、默认 Shell 是什么、UID 从哪个区间分配,不同发行版的默认值并不一致(RHEL 系默认创建家目录、Debian 系默认不创建)。生产环境创建账号时,关键参数应当显式指定,不要依赖默认值。
创建普通登录用户
bash
useradd -m -d /home/deploy -s /bin/bash deploy参数含义:
-m— 创建家目录(若家目录已存在则报错;-M表示不创建)-d— 显式指定家目录路径-s— 显式指定登录 Shell
执行后验证:
bash
$ id deploy
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy)
$ ls -la /home/deploy
total 24
drwx------ 4 deploy deploy 4096 Jun 23 10:45 .
drwxr-xr-x 5 root root 4096 Jun 23 10:45 ..
-rw-r--r-- 1 deploy deploy 220 Jun 23 10:45 .bash_logout
-rw-r--r-- 1 deploy deploy 3771 Jun 23 10:45 .bashrc
-rw-r--r-- 1 deploy deploy 807 Jun 23 10:45 .profile创建系统服务账号
为 Nginx、MySQL 这类服务创建专用账号时,通常需要"不能交互登录、不需要家目录、UID 处于系统用户区间"的账号:
bash
useradd -r -s /sbin/nologin nginx参数含义:
-r— 创建系统用户。UID 从系统用户区间分配(通常小于 1000),且不自动创建家目录-s /sbin/nologin— Shell 设为nologin,该账号不能交互登录
服务账号设置 nologin 是一项安全实践:即便服务进程被攻破,攻击者拿到的也只是低权限服务账号,而无法直接登录该账号继续操作。
Shell 为 nologin 的账号能否运行进程
nologin 只阻止交互登录,不影响该账号被用作进程的运行身份。Nginx 启动后会 fork 出 worker 进程,这些 worker 进程以 nginx 用户身份运行,不需要任何登录动作。
bash
$ ps -ef | grep nginx
root 1234 1 0 09:00 ? 00:00:00 nginx: master process /usr/sbin/nginx
nginx 1235 1234 0 09:00 ? 00:00:00 nginx: worker process
nginx 1236 1234 0 09:00 ? 00:00:00 nginx: worker process四、修改与删除用户
1. usermod 修改用户属性
bash
usermod -aG wheel deploy # 把 deploy 追加到 wheel 附加组
usermod -s /bin/bash deploy # 修改登录 Shell
usermod -d /data/deploy deploy # 修改家目录(不会移动原家目录内容)
usermod -L deploy # 锁定用户(禁止登录)
usermod -U deploy # 解锁用户2. -G 与 -aG 的陷阱
usermod -G group1,group2 user 的默认行为是用指定的组列表完全替换该用户当前的附加组列表。如果只想追加,必须加上 -a(append):
bash
# 错误:把 deploy 移出所有现有附加组,只保留 wheel
usermod -G wheel deploy
# 正确:把 deploy 追加到 wheel,保留原有附加组
usermod -aG wheel deploy这是用户管理中最容易踩的陷阱之一。例如某账号原本属于 wheel、docker、sudo 三个附加组,执行 usermod -G developers deploy 后,该账号会被从 wheel、docker、sudo 中全部移除,只剩下 developers——原本能用的 sudo 和 docker 权限瞬间消失,事故不易立即被发现。
记忆方式:任何把用户加到新组的场景,都使用 -aG,永远不要用 -G。
3. 用户删除的标准流程
bash
userdel app # 删除用户,保留家目录
userdel -r app # 同时删除家目录和邮件目录生产环境删除账号应当先锁定观察,再实际删除,避免直接 userdel 后发现仍有定时任务或服务依赖该账号:
bash
# 第 1 步:锁定账号,禁止登录
usermod -L olduser
# 第 2 步:把账号过期时间设为 1970-01-01,立即失效
chage -E 0 olduser
# 第 3 步:观察一段时间(通常 1-2 周),确认无定时任务、systemd unit 引用该账号
grep -r "olduser" /etc/cron* 2>/dev/null
grep -r "User=olduser" /etc/systemd/ 2>/dev/null
# 第 4 步:确认无引用后,正式删除
userdel -r olduser跳过这个流程直接 userdel,如果发现某个 cron 任务或 systemd 服务以该账号运行,服务会因为"账号不存在"而启动失败,恢复账号(尤其是要保持 UID 一致)非常麻烦。
五、su 与 sudo
1. su:切换身份
su 完整切换到目标用户的身份:
bash
su - app-(横杠)参数极其关键:带横杠表示加载目标用户的完整登录环境(环境变量、PATH、工作目录),不带横杠仅切换 UID/GID,环境变量保持原样。
bash
# 不带横杠
$ su app
$ pwd
/root # 工作目录还是原来的位置
$ echo $PATH
/root/bin:/usr/local/sbin:... # PATH 还是 root 的
# 带横杠
$ su - app
$ pwd
/home/app # 工作目录切换为 app 的家目录
$ echo $PATH
/home/app/bin:/usr/local/bin:... # PATH 切换为 app 的排查"su 切换后命令找不到"的问题,大多数情况都是因为忘记加 -,导致 PATH 没有切换。
2. sudo:单条命令提权
sudo 以其他用户(默认 root)身份执行单条命令,比 su 更常用也更安全——执行完即释放权限,不会长时间停留在 root 会话中:
bash
sudo systemctl restart nginx
sudo -u app whoami # 以 app 身份执行命令
sudo -i # 切换到 root 的登录会话(谨慎使用)3. sudoers 配置:必须用 visudo
sudo 的配置文件 /etc/sudoers 必须使用 visudo 编辑,不能直接 vim:
bash
visudovisudo 在保存前会做语法检查,语法错误会拒绝保存并提示。这个机制的意义在于:/etc/sudoers 一旦写错语法,所有人(包括 root 之外的所有运维)都无法使用 sudo,只能通过单用户模式或物理控制台修复——生产环境踩到这个坑会很被动。
visudo 也支持指定要编辑的文件,适合修改 /etc/sudoers.d/ 下的扩展配置:
bash
visudo -f /etc/sudoers.d/deploy4. 把用户加入管理员组
RHEL 系的管理员组叫 wheel,Debian/Ubuntu 系叫 sudo,将用户加入对应组后即可使用 sudo:
bash
# RHEL / CentOS / Rocky
usermod -aG wheel deploy
# Debian / Ubuntu
usermod -aG sudo deploy加入组后,新的组身份在该用户的现有会话中不会立即生效——需要该用户登出后重新登录,或在当前会话中执行 newgrp wheel 临时切换。
5. 细粒度授权
如果只允许某用户执行特定命令,在 sudoers 中针对命令授权:
sudoers
# 允许 deploy 用户重启 nginx 和查看 nginx 状态
deploy ALL=(root) /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx
# 允许 ops 组所有成员免密码执行 docker 命令
%ops ALL=(root) NOPASSWD: /usr/bin/docker字段含义(以第一条为例):
| 字段 | 值 | 含义 |
|---|---|---|
| 授权主体 | deploy | 哪个用户或组(组前加 %) |
| 来源主机 | ALL | 从哪些主机登录时生效 |
| 目标身份 | (root) | 以谁的身份执行 |
| 允许的命令 | /usr/bin/systemctl restart nginx | 必须写绝对路径 |
6. NOPASSWD 的使用边界
NOPASSWD: ALL 不应当用于普通账号。将所有人配置成 ALL=(ALL) NOPASSWD:ALL 操作上确实方便,但出问题时审计完全失效——sudo 日志中无法区分是谁执行了哪条具体高危命令,失去了 sudo 设计的核心价值。
生产环境的合理做法是:
- 普通日常运维:
%wheel ALL=(ALL) ALL— 需要输入密码,sudo 行为有完整审计 - 特定自动化场景:针对具体命令使用
NOPASSWD,例如 CI/CD 流水线需要免密重启服务 - 紧急救火账号:单独账号 + 单独审计,不开
NOPASSWD
六、登录与会话审计
查看当前在线用户与历史登录记录:
bash
w # 当前有哪些用户登录,正在执行什么命令
who # 简化的在线用户列表
last # 历史登录记录(读取 /var/log/wtmp)
last -n 20 # 最近 20 条登录记录
lastb # 失败登录记录(读取 /var/log/btmp)last 的输出示例:
deploy pts/0 10.1.2.34 Tue Jun 23 09:50 still logged in
admin pts/1 10.1.2.35 Tue Jun 23 08:30 - 17:42 (09:12)
deploy pts/0 10.1.2.34 Mon Jun 22 14:20 - 18:30 (04:10)
reboot system boot 5.15.0-91 Mon Jun 22 09:00 still running字段含义:用户、终端、来源 IP、登录时间、登出时间或 still logged in、会话时长。
lastb 与暴力破解
SSH 端口直接暴露在公网时,lastb 输出的失败登录记录通常非常密集——大量自动化扫描器在持续尝试常见账号(root、admin、test)和弱密码:
$ lastb -n 10
root ssh:notty 185.x.x.x Tue Jun 23 10:23 - 10:23 (00:00)
admin ssh:notty 178.x.x.x Tue Jun 23 10:22 - 10:22 (00:00)
oracle ssh:notty 91.x.x.x Tue Jun 23 10:21 - 10:21 (00:00)应对策略:
- 禁用密码登录,只允许密钥认证 — 在
/etc/ssh/sshd_config中设置PasswordAuthentication no - 限制来源 IP — 通过防火墙或 sshd 的
AllowUsers user@10.0.0.0/8限定可访问的 IP 段 - 使用堡垒机统一入口 — 业务服务器只允许堡垒机 IP 访问,SSH 端口对公网完全关闭
- 改 SSH 端口 — 不是真正的安全措施,只能减少日志噪音(降低被自动扫描器命中的概率),不能阻挡定向攻击
修改 SSH 配置的具体方法在 第 16 讲 系统安全加固 中介绍。