Skip to content

04|用户与组

第 1 讲登录机器后敲 whoami,看到自己是 root——root 权限最大,敲错命令可能删掉整个系统,生产环境里不该这么用。那该用什么身份?这就涉及"用户"的概念。

再往后会碰到更多和用户有关的问题:访问某个文件被拒"permission denied"、想用 docker 命令提示没权限、新同事要登录服务器得给他开个账号、服务进程该以什么身份跑。这些都绕不开"用户与组"。

本篇讲清 Linux 里用户和组是怎么回事、怎么管账号、susudo 有什么区别。

一、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 而非 catgetent 会走系统的名称服务交换(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 中,此处仅占位
UID0用户 ID
GID0主组 ID
GECOSroot注释字段,通常存放全名或说明
家目录/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

这是用户管理中最容易踩的陷阱之一。例如某账号原本属于 wheeldockersudo 三个附加组,执行 usermod -G developers deploy 后,该账号会被从 wheeldockersudo 中全部移除,只剩下 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
visudo

visudo 在保存前会做语法检查,语法错误会拒绝保存并提示。这个机制的意义在于:/etc/sudoers 一旦写错语法,所有人(包括 root 之外的所有运维)都无法使用 sudo,只能通过单用户模式或物理控制台修复——生产环境踩到这个坑会很被动。

visudo 也支持指定要编辑的文件,适合修改 /etc/sudoers.d/ 下的扩展配置:

bash
visudo -f /etc/sudoers.d/deploy

4. 把用户加入管理员组

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)

应对策略:

  1. 禁用密码登录,只允许密钥认证 — 在 /etc/ssh/sshd_config 中设置 PasswordAuthentication no
  2. 限制来源 IP — 通过防火墙或 sshd 的 AllowUsers user@10.0.0.0/8 限定可访问的 IP 段
  3. 使用堡垒机统一入口 — 业务服务器只允许堡垒机 IP 访问,SSH 端口对公网完全关闭
  4. 改 SSH 端口 — 不是真正的安全措施,只能减少日志噪音(降低被自动扫描器命中的概率),不能阻挡定向攻击

修改 SSH 配置的具体方法在 第 16 讲 系统安全加固 中介绍。