目录

Linux-10 用户、组与权限实操:从 useradd 到 sudoers 的每一步

目录

这一篇讲操作:怎么建用户、怎么配组、怎么给权限、怎么配 sudo。每个操作都会顺带说清「为什么是这样」,但完整的设计演进史(DAC 的缺陷、capabilities 如何拆解 root、SELinux 为什么要在 DAC 之上再加一层)留给第 23 篇。

先看五个问题:

  1. useraddadduser 有什么区别?为什么会有两个命令?
  2. 密码为什么不存在 /etc/passwd 里,非要挪到 /etc/shadow
  3. chmod 777 给了最大权限,为什么还是 Permission denied
  4. 为什么要配 sudoers,而不是直接把 root 密码告诉同事?
  5. susu -sudo -isudo -s 四个有什么区别?

1. 身份的本质:内核只认数字

1.1 uid 与 gid

id
# uid=1000(lhx) gid=1000(lhx) groups=1000(lhx),27(sudo),999(docker)
#     ^^^^ 内核认这个   ^^^^ 主组          ^^^^^^^^^^^^^^^^^^^^^ 附加组

id -u          # 1000        只要 uid
id -un         # lhx         uid 对应的名字
id -g          # 1000        主组 gid
id -G          # 1000 27 999 所有组 gid
id root        # 看别人的身份

内核在做权限检查时只比较数字,从不查 /etc/passwd。用户名只是给人看的翻译层。这个认知能解释一串现象:

# ① 删掉用户后,他创建的文件会显示成数字
ls -l /data/
# -rw-r--r-- 1 1005 1005 0 Aug 12 19:00 orphan.txt
#              ^^^^ ^^^^ 翻译不出名字了,因为 /etc/passwd 里没有这条记录了
find /data -nouser          # 专门找这类文件

# ② root 的特殊性只是「uid == 0」
# 内核里大量检查就是 if (uid == 0) return ALLOWED;
awk -F: '$3==0 {print $1}' /etc/passwd     # 列出所有 uid 为 0 的账号
# root                                      <- 正常系统只应该有这一个
#                                              多出来的就是后门账号

# ③ 容器里的 uid 和宿主机是同一套(未开 user namespace 时)
docker run --rm -v /tmp/x:/x alpine touch /x/f
ls -l /tmp/x/f          # 属主是宿主机的 root,你的普通账号删不掉

1.2 /etc/passwd:七个字段

grep -E '^(root|lhx)' /etc/passwd
# root:x:0:0:root:/root:/bin/bash
# lhx:x:1000:1000:LiHongXing,,,:/home/lhx:/bin/bash
#  |  | |    |    |             |         |
#  |  | |    |    |             |         └─ ⑦ 登录 shell
#  |  | |    |    |             └─────────── ⑥ 家目录
#  |  | |    |    └───────────────────────── ⑤ 描述信息(GECOS)
#  |  | |    └────────────────────────────── ④ 主组 gid
#  |  | └─────────────────────────────────── ③ uid
#  |  └───────────────────────────────────── ② 密码占位符(x = 去 shadow 里找)
#  └──────────────────────────────────────── ① 用户名

1.3 开篇第二问:密码为什么必须挪到 shadow

因为 /etc/passwd 必须是全局可读的。

ls -l /etc/passwd /etc/shadow
# -rw-r--r-- 1 root root   2986 Aug 12 19:00 /etc/passwd      <- 644,所有人可读
# -rw-r----- 1 root shadow 1654 Aug 12 19:00 /etc/shadow      <- 640,只有 root 和 shadow 组

/etc/passwd 承担的是「uid ↔ 用户名」的翻译职责,而这个翻译每个进程都需要

ls -l /tmp          # ls 要把 uid 1000 显示成 "lhx",它必须能读 /etc/passwd
ps aux              # 同理

如果密码哈希也放在这个全局可读的文件里,任何用户都能把它拷走离线暴力破解。1980 年代确实就是这样的,随着 CPU 变快,这成了严重漏洞。于是把密码哈希单独拆到 /etc/shadow(影子文件),权限 640。

sudo grep lhx /etc/shadow
# lhx:$y$j9T$aB3...$xYz...:19947:0:99999:7:::
#  |  |                     |     | |     | |
#  |  |                     |     | |     | └─ ⑨ 保留字段
#  |  |                     |     | |     └─── ⑧ 账号过期日期(天数,从 1970-01-01 算)
#  |  |                     |     | └───────── ⑦ 过期后的宽限天数
#  |  |                     |     └─────────── ⑥ 过期前几天开始警告(7 天)
#  |  |                     |                  ⑤ 密码最长有效天数(99999 = 不过期)
#  |  |                     └───────────────── ④ 密码最短使用天数(0 = 随时可改)
#  |  └─────────────────────────────────────── ② 密码哈希 ③ 最后修改日期
#  └────────────────────────────────────────── ① 用户名

# 哈希算法前缀
# $1$  = MD5(已不安全)
# $5$  = SHA-256
# $6$  = SHA-512(长期的默认值)
# $y$  = yescrypt(现代发行版默认,抗 GPU 破解)
# $2y$ = bcrypt
# 以 ! 或 * 开头 = 【该账号无法用密码登录】(被锁定,或本来就是服务账号)

实用推论

# 查哪些账号能用密码登录(哈希不以 ! 或 * 开头的)
sudo awk -F: '$2 !~ /^[!*]/ && $2 != "" {print $1}' /etc/shadow

# 查哪些账号有可登录的 shell
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $3, $7}' /etc/passwd
# root 0 /bin/bash
# lhx 1000 /bin/bash
# <- 其他系统账号的 shell 都是 /usr/sbin/nologin,登不进来

1.4 /etc/group 与主组、附加组

grep -E '^(sudo|docker)' /etc/group
# sudo:x:27:lhx,alice
# docker:x:999:lhx
#   |    | |   └─ ④ 成员列表(【只列附加成员】,主组成员不在这里!)
#   |    | └───── ③ gid
#   |    └─────── ② 组密码占位(几乎不用,在 /etc/gshadow)
#   └──────────── ① 组名

主组与附加组的区别

主组(primary) 附加组(supplementary)
记录在哪 /etc/passwd 第 4 字段 /etc/group 第 4 字段
数量 只有 1 个 可以多个(上限通常 65536)
作用 新建文件的默认属组 只用于权限判断
查看 id -gn id -Gn
id -gn                     # lhx        主组
id -Gn                     # lhx sudo docker    所有组
groups                     # 同 id -Gn
touch /tmp/f && ls -l /tmp/f
# -rw-r--r-- 1 lhx lhx ...            <- 属组是【主组】
#                  ^^^

1.5 uid 区间的划分

grep -E '^(UID_MIN|UID_MAX|SYS_UID_MIN|SYS_UID_MAX|GID_MIN)' /etc/login.defs
# UID_MIN                  1000
# UID_MAX                 60000
# SYS_UID_MIN               100
# SYS_UID_MAX               999
uid 范围 用途
0 root(唯一特权 uid)
1~99 传统的静态系统账号(bindaemonmail
100~999 动态分配的系统/服务账号nginxmysqlsystemd-*
1000~60000 普通用户useradd 默认从这里分配)
65534 nobody(NFS 把未知用户映射到它)
65535 (uid_t)-1,保留不用
# 建服务账号要用 -r(system),从 100~999 里分配
sudo useradd -r -s /usr/sbin/nologin myapp
id myapp        # uid=997(myapp) gid=997(myapp)

# 为什么服务账号要用系统 uid 段?
# ① 语义清晰:一眼能区分「人」和「服务」
# ② 避免与 LDAP/AD 同步过来的真实用户 uid 冲突
# ③ 某些工具(如 who、last)默认过滤掉系统 uid

2. 用户管理

2.1 开篇第一问:useradd 与 adduser

useradd adduser
是什么 底层命令(C 程序,shadow-utils 包) Debian 系的 Perl 封装脚本
交互性 非交互,纯参数驱动 交互式,会问你密码、全名等
默认建家目录 不建(要显式加 -m (并从 /etc/skel 拷模板文件)
默认设密码 不设(账号处于锁定状态) 提示你输入
默认 shell /etc/default/useraddSHELL=(常是 /bin/sh /bin/bash
可用性 所有发行版都有(POSIX 工具链) 只有 Debian/Ubuntu 有(RHEL 上 adduseruseradd 的软链接)
适用 脚本、自动化 手动交互建账号
# 手动建人类用户:Debian 上 adduser 最省事
sudo adduser alice
# Adding user `alice' ...
# Enter new UNIX password:            <- 交互式
# Full Name []:

# 脚本里/RHEL 上:用 useradd,参数要写全
sudo useradd -m -s /bin/bash -c "Alice Wang" alice
sudo passwd alice
#            ^^^ useradd 不设密码,必须单独设,否则账号无法登录

# RHEL 上的 adduser 其实就是 useradd
readlink -f "$(command -v adduser)"
# /usr/sbin/useradd

2.2 useradd 完整参数

全称 作用
-m --create-home create home 创建家目录(并拷 /etc/skel
-M --no-create-home 不创建家目录
-d DIR --home-dir home dir 指定家目录路径
-s SHELL --shell shell 登录 shell(服务账号用 /usr/sbin/nologin
-u UID --uid uid 指定 uid
-g GROUP --gid gid 指定主组
-G G1,G2 --groups groups 指定附加组(逗号分隔)
-N --no-user-group 不创建同名组(主组用 USERGROUPS_ENAB 的默认值)
-U --user-group 创建同名组作为主组(多数发行版的默认行为)
-r --system system 系统账号(uid 从 SYS_UID 段分配,不建家目录,无密码过期)
-c TEXT --comment comment 描述信息(GECOS 字段)
-e DATE --expiredate expire 账号过期日期(2026-12-31
-f N --inactive inactive 密码过期后的宽限天数
-k DIR --skel skeleton 指定模板目录(配合 -m
-p HASH --password 直接设加密后的密码哈希(不是明文
-D --defaults defaults 查看/修改默认值(存在 /etc/default/useradd
sudo useradd -D                    # 查看默认值
# GROUP=100
# HOME=/home
# INACTIVE=-1
# EXPIRE=
# SHELL=/bin/sh                    <- 注意默认是 sh 不是 bash
# SKEL=/etc/skel
# CREATE_MAIL_SPOOL=yes

sudo useradd -D -s /bin/bash       # 改默认 shell

ls -A /etc/skel/                   # 新用户家目录会拷这些文件
# .bash_logout  .bashrc  .profile

2.3 usermod:最容易出事的命令

# 💀 最经典的事故:-G 不加 -a 是【覆盖】语义
sudo usermod -G docker lhx
# 后果:lhx 被从【所有未列出的附加组】里移除!
#      如果他原本在 sudo 组,现在失去了 sudo 权限;
#      如果没有其他 root 途径,你就把自己锁在外面了

# ✅ 正确写法:-a(append)追加
sudo usermod -aG docker lhx
#             ^ append,必须加!

# 事故补救(如果还有 root 会话或能进单用户模式)
sudo usermod -aG sudo lhx          # 加回来
参数 作用
-aG G1,G2 追加附加组(-a 必须配合 -G
-G G1,G2 覆盖附加组(危险)
-g GROUP 改主组
-l NEW 改用户名(不会改家目录名和文件属主
-d DIR 改家目录(不搬文件
-d DIR -m 改家目录并搬迁内容
-s SHELL 改登录 shell
-u UID 改 uid(已有文件的属主不会自动变,要手动 chown -R
-L 锁定账号(在密码哈希前加 !
-U 解锁
-e DATE 改账号过期日期
-c TEXT 改描述
-p HASH 改密码哈希
# 改 uid 之后必须修文件属主
sudo usermod -u 2000 alice
sudo find / -xdev -uid 1001 -exec chown -h 2000 {} +      # 1001 是旧 uid
#                                          ^^ -h 处理软链接本身

# 禁用账号的三种方式(含义不同)
sudo usermod -L alice              # 锁密码(还能用 ssh 密钥登录!)
sudo usermod -s /usr/sbin/nologin alice     # 禁 shell(还能用 ssh 转发/scp)
sudo usermod -e 1970-01-01 alice   # 账号过期(最彻底,PAM 层拒绝)
sudo passwd -l alice               # 等价 usermod -L
# ✅ 彻底禁用要同时做:锁密码 + 换 nologin + 设过期 + 删 ~/.ssh/authorized_keys

2.4 userdel 与 passwd

sudo userdel alice                 # 只删账号,家目录和邮件保留
sudo userdel -r alice              # r = remove,同时删家目录和邮件池   ← 常用
sudo userdel -f alice              # 强制(即使用户还登录着)

# ⚠️ userdel 不会删除用户在其他位置的文件(/tmp、/var、/data)
sudo find / -xdev -uid 1001 2>/dev/null       # 删之前先找出来
passwd                             # 改自己的密码
sudo passwd alice                  # 改别人的(root 不需要知道旧密码)
sudo passwd -l alice               # lock 锁定
sudo passwd -u alice               # unlock 解锁
sudo passwd -d alice               # delete 删除密码(变成空密码,危险!)
sudo passwd -e alice               # expire 强制下次登录时改密码   ← 新员工入职常用
sudo passwd -S alice               # status 查看状态
# alice P 2026-08-12 0 99999 7 -1
#       ^ P=有可用密码  L=锁定  NP=无密码

# 脚本里批量设密码(不能用 passwd 的交互式输入)
echo 'alice:NewPass123' | sudo chpasswd
echo 'NewPass123' | sudo passwd --stdin alice        # 仅 RHEL 系支持 --stdin
# ⚠️ 明文密码会进 shell 历史和进程列表,生产上用密钥登录,别用密码

2.5 chage:密码策略

sudo chage -l alice                # list 查看策略
# Last password change                : Aug 12, 2026
# Password expires                    : never
# Password inactive                   : never
# Account expires                     : never
# Minimum number of days between password change : 0
# Maximum number of days between password change : 99999
# Number of days of warning before password expires : 7

sudo chage -M 90 alice             # 密码最长 90 天
sudo chage -m 7 alice              # 改密码后至少 7 天才能再改
sudo chage -W 14 alice             # 过期前 14 天开始警告
sudo chage -I 30 alice             # 过期后 30 天宽限期
sudo chage -E 2026-12-31 alice     # 账号过期日
sudo chage -d 0 alice              # 强制下次登录改密码(等价 passwd -e)
sudo chage -E -1 -M -1 alice       # 取消所有过期限制

2.6 查询命令

getent passwd alice               # 查用户(会查 LDAP/AD 等所有 NSS 数据源)
getent passwd | wc -l             # 所有用户数(比 wc -l /etc/passwd 更全)
getent group docker               # 查组
getent shadow alice               # 查影子条目(需权限)
# 为什么用 getent 而不是 grep /etc/passwd?
# 因为企业环境里用户可能来自 LDAP/AD,不在本地文件里 —— getent 走 NSS,能查到

id alice                          # 完整身份
groups alice                      # 所属组
lid -g docker                     # 组里有哪些用户(部分发行版)
members docker                    # 同上(需装 members 包)
getent group docker | cut -d: -f4 # 或者直接看第 4 字段

who                               # 当前登录的用户
w                                 # 当前登录 + 他们在干什么
last                              # 登录历史(读 /var/log/wtmp)
last alice                        # 某人的登录历史
lastlog                           # 每个用户的最后登录时间
lastb                             # 失败的登录尝试(读 /var/log/btmp)  ← 安全排查

3. 组管理

sudo groupadd developers           # 建组
sudo groupadd -g 5000 developers   # 指定 gid
sudo groupadd -r sysgroup          # 系统组(gid 从系统段分配)
sudo groupmod -n newname oldname   # 改名
sudo groupmod -g 5001 developers   # 改 gid(已有文件的属组不会自动变)
sudo groupdel developers           # 删组(组是某人的主组时会拒绝)

# 添加/移除成员
sudo usermod -aG developers alice        # ✅ 推荐
sudo gpasswd -a alice developers         # 等价
sudo gpasswd -d alice developers         # 移除成员
sudo gpasswd -M alice,bob developers     # 设置成员列表(覆盖)
sudo gpasswd -A alice developers         # 设置组管理员(可自行增删成员)

3.1 为什么改组后要重新登录

sudo usermod -aG docker lhx
docker ps
# permission denied while trying to connect to the Docker daemon socket
#  ^^ 明明加了组,为什么不生效?

id                       # 输出里确实有 docker?
# uid=1000(lhx) gid=1000(lhx) groups=1000(lhx),27(sudo)
#                                                     ^^ 没有 docker!

getent group docker      # docker:x:999:lhx        <- 文件里明明有

原因:组成员关系是在登录时读取并写入进程凭证的。 内核给每个进程保存一份「附加组列表」(supplementary groups),它在 setgroups() 时确定,之后不会自动刷新。你当前的 shell 是加组之前登录的,它的凭证里没有 docker。

# 三种解决办法
exit && ssh host                 # ✅ 重新登录(最彻底)
newgrp docker                    # ✅ 开一个新 shell,凭证里加上 docker
                                 #    (只影响这个新 shell,exit 后回到旧的)
sg docker -c 'docker ps'         # ✅ 只用新组身份执行一条命令

# 验证凭证确实是进程属性
cat /proc/$$/status | grep Groups
# Groups: 27 1000                <- 就是这一行,内核里存的

这个机制也是很多「权限改了但不生效」问题的答案 —— 凭证在进程创建时快照,之后不跟随文件变化。

3.2 共享目录:SGID 的经典用法

需求:团队目录里所有人建的文件,组内成员都能读写。

sudo groupadd devteam
sudo usermod -aG devteam alice
sudo usermod -aG devteam bob

sudo mkdir -p /srv/share
sudo chgrp devteam /srv/share
sudo chmod 2775 /srv/share
#            ^ 这个 2 就是 SGID
ls -ld /srv/share
# drwxrwsr-x 2 root devteam 4096 Aug 12 19:00 /srv/share
#       ^ s 出现在 group 的 x 位置

# 效果:任何人在里面建的文件,属组自动是 devteam(而不是创建者的主组)
sudo -u alice touch /srv/share/from_alice
ls -l /srv/share/
# -rw-r--r-- 1 alice devteam 0 Aug 12 19:00 from_alice
#                    ^^^^^^^ 继承了目录的属组  ✅

还差一步:文件权限是 644,组成员只能读不能写。

# 原因:新建文件的权限受 umask 影响(默认 022 -> 组写权限被削掉)
umask                    # 0022
# ✅ 相关用户的 umask 要设成 002
echo 'umask 002' | sudo tee -a /etc/profile.d/share-umask.sh
# 或者只对特定组生效(需要 pam_umask 或在用户的 .bashrc 里设)

# 之后新建的文件才是 664
sudo -u alice bash -c 'umask 002; touch /srv/share/f2'
ls -l /srv/share/f2
# -rw-rw-r-- 1 alice devteam 0 ...       ✅ 组内可写

# 如果目录还允许所有人写(如上传目录),必须再加 Sticky 位防止互删
sudo chmod 3775 /srv/share       # 2(SGID) + 1(Sticky)
# 或 sudo chmod g+s,o+t /srv/share

更彻底的方案是用 ACL 的默认条目(见第 6 章),它能精确控制继承的权限位而不依赖 umask。

4. 权限位:rwx 的两套语义

4.1 读懂 ls -l

-rw-r--r--  1  lhx  dev  1024  Aug 12 19:00  file
drwxr-xr-x  2  lhx  dev  4096  Aug 12 19:00  dir
^^^^^^^^^^
│└─┬┘└─┬┘└┬┘
│  │   │  └── other:其他所有人
│  │   └───── group:属组成员
│  └───────── user:属主
└──────────── 类型:- 文件  d 目录  l 软链接  c/b 设备  s socket  p 管道

权限检查的顺序是**「三选一」,不是叠加**:

内核检查一个进程能否访问文件:
   ① 进程的 euid == 文件的属主 uid ?  -> 用 user 那 3 位判断,【结束】
   ② 进程的 egid 或附加组 包含 文件的属组 gid ?  -> 用 group 那 3 位判断,【结束】
   ③ 否则 -> 用 other 那 3 位判断

这带来一个反直觉的结果

touch f && chmod 077 f          # 属主无权限,组和其他人全权限
ls -l f
# ----rwxrwx 1 lhx lhx 0 Aug 12 19:00 f
cat f
# cat: f: Permission denied     <- 你是属主,只看 user 那 3 位(全空),直接拒绝!
#                                  即使你也在 lhx 组里也不行

4.2 开篇第三问:文件与目录的 rwx 完全不同

文件 目录
r 读取内容 列出里面有哪些名字(仅名字!)
w 修改内容 在里面创建/删除/重命名条目
x 执行这个程序 通行权:能否「穿过」这个目录访问里面的东西

目录的 x 是最关键的一位,它决定能否 stat 里面的文件、能否 cd 进去、能否把它作为路径的一部分:

mkdir -p /tmp/d && touch /tmp/d/f && echo data > /tmp/d/f

# 只有 r 没有 x
chmod 444 /tmp/d
ls /tmp/d                        # f          <- 能列出名字
ls -l /tmp/d
# ls: cannot access '/tmp/d/f': Permission denied
# -????????? ? ? ? ?            ? f          <- 拿不到任何元数据!
cd /tmp/d
# bash: cd: /tmp/d: Permission denied
cat /tmp/d/f
# cat: /tmp/d/f: Permission denied            <- 走不进去

# 只有 x 没有 r
chmod 111 /tmp/d
ls /tmp/d
# ls: cannot open directory '/tmp/d': Permission denied     <- 不能列目录
cat /tmp/d/f
# data                                        <- 但【知道确切文件名】就能访问!

最后这个特性有实际用途:家目录设成 701,别人无法列出你有哪些文件(隐私),但 Web 服务器仍能通过确切路径访问 ~/public_html/index.html

开篇第三问的答案chmod 777 file 只改了文件本身,如果它所在的某一级父目录x,你根本走不到这个文件。用 namei -l(03 篇 5.4)一眼定位:

namei -l /var/www/html/index.html
# f: /var/www/html/index.html
# dr-xr-xr-x root root /
# drwxr-xr-x root root var
# drwx------ root root www          <- 就是这一层!other 没有 x
# drwxr-xr-x root root html
# -rwxrwxrwx root root index.html   <- 文件是 777 也没用

4.3 删除文件看的是目录权限

这条也很反直觉(04 篇 3.2 提过,这里补完整):

mkdir /tmp/t && echo x > /tmp/t/f
chmod 444 /tmp/t/f               # 文件设为只读
rm /tmp/t/f
# rm: remove write-protected regular file '/tmp/t/f'? y
# <- 只是问一下,回车就删了

chmod 555 /tmp/t                 # 目录去掉 w
touch /tmp/t/f2
# touch: cannot touch '/tmp/t/f2': Permission denied
rm -f /tmp/t/f
# rm: cannot remove '/tmp/t/f': Permission denied      <- 这才真删不掉

原因:删除是 unlink(),改的是目录里的那一行记录,所以检查的是目录的 w 权限。这也意味着:你对一个 root 的只读文件毫无权限,只要目录是你的,你就能删掉它。

这个「漏洞」正是 Sticky 位被发明的原因(见 5.3)。

4.4 chmod:两套语法

数字法(八进制)

r = 4    w = 2    x = 1

rwx = 4+2+1 = 7
rw- = 4+2   = 6
r-x = 4+1   = 5
r-- = 4     = 4
-wx = 2+1   = 3
chmod 644 file          # rw-r--r--    普通文件的标准权限
chmod 600 file          # rw-------    私密文件(ssh 密钥、密码文件)
chmod 755 script.sh     # rwxr-xr-x    可执行文件/目录的标准权限
chmod 700 dir           # rwx------    私有目录
chmod 640 config.yaml   # rw-r-----    属主可写,组可读,其他人不可见
chmod 777 file          # ⚠️ 几乎永远是错的,说明你没想清楚该给谁权限
chmod 4755 file         # SUID + 755(4 位数时第一位是特殊权限位)

符号法(增量修改,不影响其他位)

chmod u+x script.sh     # 给属主加执行权    u=user g=group o=other a=all
chmod g-w file          # 去掉组的写权限
chmod o= file           # 清空其他人的所有权限
chmod a+r file          # 所有人可读
chmod u=rw,go=r file    # 精确设定(= 是覆盖,+ 是追加)
chmod g+w,o-rwx dir
chmod +x script.sh      # 不写 ugo 时受 umask 影响,等价 a+x 但会去掉 umask 屏蔽的位

数字法 vs 符号法的选择

# 数字法:一次设定全部 9 位,结果确定
chmod 644 file          # 无论原来是什么,现在一定是 644

# 符号法:只改指定的位,其余保留
chmod +x file           # 原来是 644 -> 755;原来是 600 -> 700
# ✅ 批量操作时符号法更安全(不会意外清掉别的位)

4.5 递归 chmod 的正确做法

# ❌ 危险:给所有文件都加了执行权限
chmod -R 755 /var/www
# 后果:所有 .html、.jpg、.conf 都变成可执行文件。
#      不只是「不整洁」,某些 Web 服务器配置下可执行文件会被当作 CGI 执行 —— 安全漏洞

# ✅ 方案一:用大写 X(只给【目录】和【本来就有执行权限的文件】加 x)
chmod -R u=rwX,go=rX /var/www
#              ^ 大写 X 是这个场景的专用设计
# 效果:目录 755,普通文件 644,原本可执行的脚本保持可执行

# ✅ 方案二:find 分别处理(最明确)
find /var/www -type d -exec chmod 755 {} +
find /var/www -type f -exec chmod 644 {} +
# 需要保留脚本可执行时再单独处理
find /var/www -type f -name '*.sh' -exec chmod 755 {} +

# ✅ 方案三:用 --reference 参照某个文件的权限
chmod --reference=/etc/hosts myfile

其他有用参数:

chmod -c 644 file       # changes:只在实际改变时输出
chmod -v 644 file       # verbose:总是输出
chmod --preserve-root -R 777 /       # 拒绝对 / 递归操作(默认已开)

4.6 chown / chgrp

chown alice file                 # 改属主
chown alice:devteam file         # 改属主和属组
chown :devteam file              # 只改属组(等价 chgrp)
chown alice. file                # 属主改 alice,属组改成 alice 的主组
chgrp devteam file               # 只改属组

chown -R alice:alice /home/alice        # 递归
chown -h alice link                     # h = 改【软链接本身】而不是目标
chown --reference=/etc/hosts f          # 参照另一个文件
chown -c alice f                        # 只在变化时输出

# ⚠️ 只有 root 能改属主(普通用户不能把自己的文件"送"给别人)
#    原因:防止绕过磁盘配额,也防止塞给别人一个恶意的 SUID 文件
# 普通用户可以改属组,但只能改成自己所属的组

4.7 umask:新建文件的默认权限

umask                   # 0022
umask -S                # u=rwx,g=rx,o=rx      符号形式
umask 077               # 设成更严格(新文件 600,新目录 700)
umask 002               # 组协作场景

计算规则(这是常见的困惑点):

文件的基础权限是 666(rw-rw-rw-),【不是 777】
目录的基础权限是 777(rwxrwxrwx)

实际权限 = 基础权限 AND (NOT umask)

umask 022 时:
   文件:666 & ~022 = 644  (rw-r--r--)
   目录:777 & ~022 = 755  (rwxr-xr-x)

umask 077 时:
   文件:666 & ~077 = 600
   目录:777 & ~077 = 700

为什么文件的基础权限是 666 而不是 777? 内核规定新建文件默认不带执行权限,避免数据文件被误当程序运行。想让文件可执行必须显式 chmod +x

# 验证
umask 022; touch f1; mkdir d1; ls -ld f1 d1
# -rw-r--r-- 1 lhx lhx    0 ... f1        <- 644
# drwxr-xr-x 2 lhx lhx 4096 ... d1        <- 755

umask 077; touch f2; mkdir d2; ls -ld f2 d2
# -rw------- 1 lhx lhx    0 ... f2        <- 600
# drwx------ 2 lhx lhx 4096 ... d2        <- 700

三个要点

# ① umask 是【进程属性】,会被子进程继承
# 所以 cron、systemd 下的 umask 可能与你的交互式 shell 不同!
# 这是「手动跑权限对、定时任务权限不对」的常见原因
systemctl show myapp -p UMask            # 看 systemd 服务的 umask
# 在 unit 里显式设置:
# [Service]
# UMask=0027

# ② umask 只影响【新建】文件,对已有文件无效

# ③ 持久化设置的位置
# 全局:/etc/profile、/etc/login.defs 的 UMASK
# 用户:~/.bashrc、~/.profile
# PAM:/etc/pam.d/common-session 里的 pam_umask

5. 三个特殊权限位

    SUID   SGID   Sticky
     4      2       1
    ┌──┐  ┌──┐   ┌──┐
   4755    2775    1777
   ^        ^       ^

5.1 SUID:以文件属主的身份运行

ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 Nov 24  2022 /usr/bin/passwd
#    ^ 这个 s 就是 SUID(占用了属主的 x 位)

为什么 passwd 需要 SUID? 因为改密码要写 /etc/shadow,而它的权限是 640 root:shadow —— 普通用户完全没有写权限。但「用户改自己的密码」是合理需求。

SUID 的作用是:这个程序被任何人执行时,进程的有效 uid(euid)变成文件属主的 uid。所以 passwd 运行时 euid 是 0,能写 /etc/shadow;程序内部再检查「你只能改自己的密码」。

# 观察 euid 的变化
ls -l /usr/bin/passwd            # 属主是 root,有 SUID
# 普通用户执行 passwd 时,该进程的 euid = 0

# 找出系统上所有 SUID 程序(安全审计必做)
find / -perm -4000 -type f -ls 2>/dev/null
# /usr/bin/passwd  /usr/bin/sudo  /usr/bin/su  /usr/bin/mount  /usr/bin/newgrp ...
# ✅ 正常系统就这十几个,多出来的要立刻排查

SUID 是提权漏洞的主要入口,因为它把「普通用户能触发的代码」和「root 权限」绑在了一起。一旦这个程序有缓冲区溢出、命令注入、或允许用户指定任意路径/命令,就是直接的 root 提权。

# 加固手段
# ① 定期审计(与基线对比)
find / -perm -4000 -type f 2>/dev/null | sort > /tmp/suid.now
diff /etc/suid.baseline /tmp/suid.now

# ② 给用户可写的分区加 nosuid 挂载选项
mount -o remount,nosuid,nodev /home
# /etc/fstab: /dev/vdb1 /home ext4 defaults,nosuid,nodev 0 2
#                                           ^^^^^^ 这个分区上的 SUID 位全部失效

# ③ 自己的程序【永远不要】用 SUID,改用 capabilities(第 23 篇)或 systemd
setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
#      ^^ 只给「绑定低端口」这一个能力,而不是整个 root

注意:SUID 对脚本无效(Linux 内核直接忽略脚本的 SUID 位),因为脚本的 SUID 存在无法修补的竞态漏洞。需要脚本提权只能通过 sudo。

5.2 SGID:两种完全不同的含义

# 在【文件】上:以文件属组的身份运行(类比 SUID)
ls -l /usr/bin/wall
# -rwxr-sr-x 1 root tty ... /usr/bin/wall
#       ^ s 在 group 的 x 位 -> 运行时 egid = tty 组

# 在【目录】上:新建文件自动继承目录的属组(完全不同的语义!)
chmod g+s /srv/share            # 或 chmod 2775
# 用途见 3.2 的共享目录

目录上的 SGID 还有一个附带效果:在该目录里新建的子目录也会自动带上 SGID,所以继承是递归生效的。

5.3 Sticky 位:只能删自己的文件

ls -ld /tmp
# drwxrwxrwt 20 root root 4096 Aug 12 19:00 /tmp
#          ^ t 就是 Sticky(占用 other 的 x 位)

为什么 /tmp 需要它? /tmp 必须人人可写(777),但根据 4.3 讲的规则——删除文件只看目录的 w 权限——那么任何用户都能删掉别人的临时文件,这显然不可接受。

Sticky 位就是给这个规则打的补丁:目录带 Sticky 时,只有「文件属主」「目录属主」「root」能删除或重命名文件。

# 验证
sudo -u alice touch /tmp/alice_file
sudo -u bob rm /tmp/alice_file
# rm: cannot remove '/tmp/alice_file': Operation not permitted    ✅

# 给上传目录加 Sticky
sudo chmod 1777 /srv/upload      # 或 chmod +t /srv/upload
sudo chmod 3775 /srv/share       # SGID + Sticky 组合(团队共享 + 防互删)

5.4 大写的 S 和 T 表示配错了

chmod 4644 f && ls -l f
# -rwSr--r--                     <- 【大写 S】
#    ^ 设了 SUID 但文件本身没有 x 权限 -> 这个 SUID 毫无意义,配错了

chmod 4744 f && ls -l f
# -rwsr--r--                     <- 小写 s:SUID 生效

chmod 1644 d && ls -ld d
# drw-r--...T                    <- 【大写 T】:设了 Sticky 但 other 没有 x

# 记忆:小写 = 底层的 x 存在且特殊位生效;大写 = 底层没有 x,特殊位形同虚设

5.5 特殊位的设置与查找

# 设置(数字法:4 位数的第一位)
chmod 4755 f            # SUID
chmod 2775 d            # SGID
chmod 1777 d            # Sticky
chmod 6755 f            # SUID + SGID

# 设置(符号法)
chmod u+s f             # SUID
chmod g+s d             # SGID
chmod +t d              # Sticky
chmod u-s f             # 去掉

# 查找
find / -perm -4000 -type f 2>/dev/null       # SUID
find / -perm -2000 -type f 2>/dev/null       # SGID
find / -perm -1000 -type d 2>/dev/null       # Sticky 目录
find / -perm /6000 -type f 2>/dev/null       # SUID 或 SGID(/ 是"任意一位匹配")

6. ACL:超越 ugo 的细粒度授权

6.1 为什么需要 ACL

传统权限模型只能表达三方:属主、一个属组、其他所有人。碰到这种需求就无解了:

文件 report.xlsx 属于 alice。要让 bob 能读写、carol 只能读、审计组能读、其他人完全看不到。

用 ugo 做不到——你只有一个「属组」的位置。ACL(Access Control List)就是为此补充的机制。

6.2 基本操作

# 查看
getfacl report.xlsx
# # file: report.xlsx
# # owner: alice
# # group: alice
# user::rw-
# group::r--
# other::---

# 授权
setfacl -m u:bob:rw report.xlsx           # m = modify,给用户 bob 读写
setfacl -m u:carol:r report.xlsx          # carol 只读
setfacl -m g:audit:r report.xlsx          # 给 audit 组读
setfacl -m o::- report.xlsx               # 其他人无权限

getfacl report.xlsx
# user::rw-
# user:bob:rw-              <- 额外的用户条目
# user:carol:r--
# group::r--
# group:audit:r--
# mask::rw-                 <- 见下
# other::---

ls -l report.xlsx
# -rw-rw----+ 1 alice alice 8192 Aug 12 19:00 report.xlsx
#           ^ 这个 + 表示「有 ACL」  ← 看到 + 就该用 getfacl 而不是 ls

6.3 mask:容易踩的坑

# mask 是所有「命名条目 + 属组」的【权限上限】
setfacl -m m::r report.xlsx               # 把 mask 设为只读
getfacl report.xlsx
# user:bob:rw-              #effective:r--
#                            ^^^^^^^^^^^^^^ bob 实际只能读了!被 mask 削掉了

# chmod 会修改 mask(这是最容易出事的地方)
chmod 640 report.xlsx                     # 看起来只是改 group 位
getfacl report.xlsx | grep mask
# mask::r--                               <- 实际改的是 mask,所有 ACL 条目被降级

# ✅ 所以有 ACL 的文件,改权限要用 setfacl 而不是 chmod
setfacl -m m::rw report.xlsx              # 显式调整 mask

6.4 默认 ACL:实现继承

# -d = default,给目录设默认 ACL -> 里面【新建】的文件自动继承
setfacl -d -m u:bob:rw /srv/project
setfacl -d -m g:devteam:rwx /srv/project

# 递归应用到已有内容 + 设置默认(部署时的标准写法)
setfacl -R -m u:bob:rwX /srv/project      # R = 递归,X = 只给目录和已可执行文件加 x
setfacl -R -d -m u:bob:rwX /srv/project   # 同时设默认,新建的也继承

getfacl /srv/project
# default:user:bob:rw-                    <- default: 前缀表示这是继承规则

# 这比 SGID + umask 的组合更精确:能指定具体用户、能指定具体权限位

6.5 其他操作

setfacl -x u:bob report.xlsx              # x = 删除某条 ACL
setfacl -b report.xlsx                    # b = 删除【所有】ACL(回到纯 ugo)
setfacl -k /srv/project                   # k = 只删默认 ACL
setfacl --restore=backup.acl              # 从备份恢复
getfacl -R /srv > backup.acl              # 备份整棵树的 ACL

# 复制 ACL 到另一个文件
getfacl f1 | setfacl --set-file=- f2

# ⚠️ 注意事项
# ① 文件系统必须支持并启用 ACL(ext4/xfs 默认支持;挂载选项 acl,现在通常默认开)
mount | grep -w /       # 早期需要显式 acl 选项
tune2fs -l /dev/vda1 | grep 'Default mount options'
# ② cp 默认【不保留】ACL,要用 cp -a 或 cp --preserve=all
# ③ rsync 要用 -A 才保留 ACL(-a 不含)
rsync -aA src/ dst/
# ④ tar 要用 --acls
tar --acls -cf backup.tar /srv/project

7. sudo:受控提权

7.1 开篇第四问:为什么不直接给 root 密码

共享 root 密码 配 sudo
审计 日志里全是 root不知道是谁干的 记录「谁、在哪、执行了什么」
最小权限 给了就是全部权限 可以只授权特定命令
撤销 要改密码并通知所有人 删一行配置即可
离职处理 必须改密码 删用户就行
密码管理 多人共享一个密码,泄漏难追溯 各人用自己的密码
误操作 全程 root,一个 rm -rf 就完了 只在需要时提权,日常是普通用户

现代发行版(Ubuntu、macOS)默认根本不给 root 设密码(哈希是 !,无法直接登录),一切通过 sudo。

sudo grep '^root' /etc/shadow
# root:!:19947:0:99999:7:::
#       ^ ! 表示无法用密码登录

7.2 必须用 visudo

sudo visudo                              # ✅ 编辑主配置
sudo visudo -f /etc/sudoers.d/deploy     # ✅ 编辑片段文件
sudo visudo -c                           # 检查所有配置的语法

为什么不能直接 vim /etc/sudoers 因为 visudo 做两件关键的事:

  1. 保存时做语法检查,语法错误会提示你重新编辑而不是直接写坏文件
  2. 加锁,防止两个人同时编辑

一旦 /etc/sudoers 语法错误,sudo 会完全拒绝工作——如果你没有其他 root 途径(没设 root 密码、没有其他 sudo 用户),就把自己彻底锁在门外了,只能进单用户模式或用 live CD 修复。

# 万一锁死了的补救途径
pkexec visudo                    # 如果有 polkit
# 或重启进 GRUB -> 编辑内核参数加 init=/bin/bash -> 挂载 rw -> 修复
# 或用救援盘挂载根分区修改

7.3 sudoers 语法

# 基本格式
用户  主机=(可切换到的用户:可切换到的组)  命令列表

root    ALL=(ALL:ALL) ALL
#  |     |    |   |    └─ 允许执行的命令(ALL = 任何)
#  |     |    |   └────── 可以切换到的组
#  |     |    └────────── 可以切换到的用户
#  |     └─────────────── 在哪些主机上生效(单机时写 ALL)
#  └───────────────────── 用户名

%sudo   ALL=(ALL:ALL) ALL          # % 前缀表示【组】
%wheel  ALL=(ALL) ALL              # RHEL 系用 wheel 组,Debian 系用 sudo 组
#                                    这是两大家族的一个差异点

实用示例

# 部署账号只能重启这一个服务,且不需要输密码
deploy ALL=(root) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl status myapp

# 运维组可以查看所有日志
%ops ALL=(root) /usr/bin/journalctl, /usr/bin/tail, /usr/bin/less

# 允许切换到特定服务账号(而不是 root)
%devs ALL=(myapp) ALL              # sudo -u myapp <任意命令>

# 命令别名(便于复用)
Cmnd_Alias SERVICES = /bin/systemctl start *, /bin/systemctl stop *, /bin/systemctl restart *
Cmnd_Alias READONLY = /usr/bin/journalctl, /usr/bin/tail, /bin/cat /var/log/*
User_Alias ADMINS = alice, bob
Host_Alias WEBSERVERS = web01, web02

ADMINS WEBSERVERS = (root) SERVICES, READONLY

# 全局选项
Defaults        env_reset                    # 清空环境变量(安全)
Defaults        secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults        timestamp_timeout=15         # 免密时间窗口(分钟),0 = 每次都问
Defaults        passwd_tries=3               # 密码尝试次数
Defaults        logfile="/var/log/sudo.log"  # 单独的日志文件
Defaults        requiretty                   # 要求有 tty(阻止通过脚本调用)
Defaults:deploy !requiretty                  # 对特定用户取消(CI 场景需要)
Defaults        log_input,log_output         # 记录完整的输入输出(审计强需求)

7.4 用 sudoers.d 而不是改主文件

# ✅ 推荐做法:每个需求一个独立文件
sudo visudo -f /etc/sudoers.d/deploy
# deploy ALL=(root) NOPASSWD: /bin/systemctl restart myapp

sudo ls -l /etc/sudoers.d/
# -r--r----- 1 root root 68 Aug 12 19:00 deploy
#  ^^^^^^^^^ 权限必须是 0440,否则 sudo 会忽略它

# 好处:
# ① 配置管理工具(Ansible/Puppet)可以幂等地投放文件
# ② 一个文件写坏不影响其他规则
# ③ 删除授权 = 删文件
# ④ 包管理器升级时不会与主文件冲突

# ⚠️ 文件名不能含点(.)或以 ~ 结尾,否则被忽略
sudo ls /etc/sudoers.d/deploy.conf     # ❌ 这个文件会被无视!

7.5 危险的授权:sudo vim = 完整 root

这是最重要的一条安全认知。 很多程序内置了「执行外部命令」的能力,一旦用 sudo 运行就等于给了 root shell:

# 你以为只是让他改配置
%ops ALL=(root) /usr/bin/vim /etc/nginx/nginx.conf

# 实际上他可以
sudo vim /etc/nginx/nginx.conf
# 然后在 vim 里输入:
:!/bin/bash            # 直接开一个 root shell
:w /etc/sudoers        # 写任意文件
:r /etc/shadow         # 读任意文件

同类的「危险命令」清单(这类知识有个专门的站点叫 GTFOBins):

命令 逃逸方式
vim / vi / nvim :!sh:w /etc/sudoers
less / more / man !sh
find -exec /bin/sh \;
awk BEGIN{system("/bin/sh")}
tar --to-command=/bin/sh--checkpoint-action=exec=sh
git -c core.pager='!sh' log--exec-path
python/perl/ruby/node -c 'import os;os.system("sh")'
env env /bin/sh
nmap --script(旧版有交互模式)
systemctl edit 子命令能起编辑器
apt / yum 能执行钩子脚本
docker docker run -v /:/host --privileged 直接拿宿主机 root
mount 挂载恶意文件系统
任何能写任意文件的命令 /etc/sudoers~root/.ssh/authorized_keys/etc/cron.d/*

授权原则:只授权那些无法执行任意命令、也无法写任意路径的程序。

# ✅ 相对安全的授权
deploy ALL=(root) NOPASSWD: /bin/systemctl restart myapp
#                           ^^ 指定了具体服务名,不是 systemctl *
%ops ALL=(root) /usr/bin/journalctl -u myapp

# ⚠️ 通配符要小心
%ops ALL=(root) /bin/systemctl restart *
# 这允许 restart 任何服务,可能包括不该动的(但比 systemctl ALL 好)

# ❌ 千万不要
%ops ALL=(root) /bin/chmod, /bin/chown       # 能改任意文件权限 = 提权
%ops ALL=(root) /usr/bin/vim                 # = root shell
%ops ALL=(root) /bin/su                      # = root shell
%ops ALL=(root) /usr/bin/docker              # = 宿主机 root

需要编辑配置文件时,用 sudoedit

%ops ALL=(root) sudoedit /etc/nginx/nginx.conf

# 用户执行
sudoedit /etc/nginx/nginx.conf
# 或 sudo -e /etc/nginx/nginx.conf

# 它的机制:把文件拷到临时位置 -> 用【普通用户权限】启动编辑器 -> 写回时才提权
# 关键区别:编辑器进程不是 root,所以 :!sh 只能得到普通 shell

7.6 审计日志

# sudo 的日志默认进 syslog
sudo grep sudo /var/log/auth.log | tail        # Debian 系
sudo journalctl -t sudo | tail                 # systemd
sudo grep sudo /var/log/secure | tail          # RHEL 系

# 日志内容
# Aug 12 19:00:01 host sudo: lhx : TTY=pts/0 ; PWD=/home/lhx ; USER=root ;
#     COMMAND=/bin/systemctl restart nginx
#             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 谁、在哪个终端、在哪个目录、切换到谁、执行了什么

# 失败的尝试也会记录(安全监控重点)
sudo grep -i 'authentication failure\|NOT in the sudoers' /var/log/auth.log

# 完整的输入输出录制(高安全环境)
# sudoers 里加:Defaults log_input,log_output
sudo ls /var/log/sudo-io/                      # 录制文件
sudoreplay -l                                  # 列出所有会话
sudoreplay <TSID>                              # 【回放】某个会话,像录像一样

7.7 开篇第五问:su / su - / sudo -i / sudo -s

su                   # 切换到 root,【保留当前环境变量和 cwd】,需要 root 密码
su -                 # 切换到 root 并【模拟完整登录】(读 root 的 profile,cwd 变 /root)
su - alice           # 切换到 alice 并模拟登录
su -c 'cmd' alice    # 以 alice 身份执行一条命令

sudo -i              # 用【自己的密码】提权并模拟 root 登录(等价 su -,但走 sudo 审计)
sudo -s              # 用自己的密码提权,开一个 shell 但【保留当前环境】(等价 su)
sudo -u alice cmd    # 以 alice 身份执行命令
sudo -u alice -i     # 以 alice 身份完整登录
sudo -E cmd          # 保留全部环境变量(需 sudoers 允许,权限较松)
sudo -k              # 清除免密时间戳(下次必须重新输密码)
sudo -v              # 刷新时间戳(延长免密窗口)
sudo -l              # 列出自己被授权的命令   ← 到新环境第一件事
sudo -ll             # 更详细的格式
sudo -U alice -l     # 查看 alice 被授权什么(需权限)

对照表

用谁的密码 环境变量 cwd PATH 有审计
su root 的 保留当前的 不变 保留当前的
su - root 的 重置为 root 的 /root root 的
sudo -s 自己的 保留当前的 不变 保留当前的 ✅ 强
sudo -i 自己的 重置为 root 的 /root root 的 ✅ 强

推荐 sudo -i:审计完整,且环境干净(避免继承你自己的 PATHLD_PRELOAD 等可能导致意外的变量)。

# 一个常见困惑:为什么 sudo 后 PATH 变了(01 篇 4.3 讲过)
sudo env | grep PATH
# PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
#      ^^ 用的是 sudoers 里的 secure_path,不是你的 PATH
# 这是防提权设计:防止你用篡改的 PATH 让 sudo 执行伪造的程序

8. 实战:给 Go 服务创建专用账号

把前面的内容串成一个完整流程。假设服务叫 myapp

# ── ① 创建专用系统账号(不能登录、无家目录)──
sudo useradd -r -s /usr/sbin/nologin -M -c "MyApp service account" myapp
#            ^^ 系统账号(uid < 1000)
#               ^^^^^^^^^^^^^^^^^^^^^^ 不能登录
#                                      ^^ 不建家目录
id myapp
# uid=997(myapp) gid=997(myapp) groups=997(myapp)

# 为什么不用 root 跑服务?
# 一旦服务被 RCE(远程代码执行),攻击者直接拿到 root。
# 用专用账号则攻击面被限制在这个账号的权限范围内。

# ── ② 创建目录结构(呼应 03 篇第 4 章的 FHS 规范)──
sudo install -d -m 755 -o root  -g root  /usr/local/bin
sudo install -d -m 750 -o root  -g myapp /etc/myapp
sudo install -d -m 750 -o myapp -g myapp /var/lib/myapp
sudo install -d -m 750 -o myapp -g myapp /var/log/myapp
#            ^^ 用 install 一步完成 mkdir + chmod + chown

# ── ③ 部署二进制与配置 ──
sudo install -m 755 -o root -g root ./myapp /usr/local/bin/myapp
sudo install -m 640 -o root -g myapp ./config.yaml /etc/myapp/config.yaml
#            ^^^ 配置含密钥时用 640:root 可写,myapp 组可读,其他人看不见

# ── ④ 如果需要绑定 80/443 低端口 ──
# ❌ 不要为此用 root 运行整个服务
# ✅ 方案一:给二进制加 capability(只给"绑定低端口"这一个能力)
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
getcap /usr/local/bin/myapp
# /usr/local/bin/myapp cap_net_bind_service=ep

# ✅ 方案二:让 systemd 来做(更推荐,不用改文件属性)
# [Service]
# AmbientCapabilities=CAP_NET_BIND_SERVICE

# ✅ 方案三:前面挂 nginx 反代,服务自己听 8080

# ── ⑤ systemd 单元 ──
sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
[Unit]
Description=MyApp Go Service
After=network-online.target

[Service]
Type=notify
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure
UMask=0027

# systemd 自动创建并设好属主,服务停止时按需清理
RuntimeDirectory=myapp
StateDirectory=myapp
LogsDirectory=myapp

# 权限加固(最小权限原则的具体落地)
NoNewPrivileges=yes           # 禁止获得新特权(SUID 失效)
ProtectSystem=strict          # /usr /boot /etc 变只读
ProtectHome=yes               # 看不到 /home
PrivateTmp=yes                # 独立的 /tmp
PrivateDevices=yes            # 只有基本的 /dev
ProtectKernelTunables=yes     # /proc/sys 只读
ProtectKernelModules=yes      # 禁止加载内核模块
ProtectControlGroups=yes      # cgroup 只读
RestrictSUIDSGID=yes          # 禁止创建 SUID 文件
RestrictNamespaces=yes        # 禁止创建 namespace
LockPersonality=yes
MemoryDenyWriteExecute=yes    # 禁止可写可执行内存(防注入;JIT 语言不能开)
SystemCallFilter=@system-service   # 系统调用白名单
ReadWritePaths=/var/lib/myapp /var/log/myapp    # 显式允许写的路径

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp

# 检查加固效果的评分
systemd-analyze security myapp
# → 会给出 0~10 的风险评分和每一项的建议

# ── ⑥ 给部署账号最小的 sudo 权限 ──
sudo visudo -f /etc/sudoers.d/deploy
# deploy ALL=(root) NOPASSWD: /bin/systemctl restart myapp
# deploy ALL=(root) NOPASSWD: /bin/systemctl status myapp
# deploy ALL=(root) NOPASSWD: /bin/journalctl -u myapp *
sudo -u deploy sudo -l          # 验证授权

这套流程体现的三条原则

  1. 最小权限:专用账号 + 只给必需的 capability + systemd 沙箱
  2. 纵深防御:即使服务被攻破,还有 ProtectSystemNoNewPrivilegesSystemCallFilter 挡着
  3. 可审计:一切通过 sudo 和 systemd,操作有日志

9. 知识点扩展

9.1 用户管理命令

命令 全称 作用
useradd user add 底层建用户命令(脚本用)
adduser Debian 的交互式封装(RHEL 上是 useradd 的链接)
usermod user modify 修改用户(-aG-a 千万别忘
userdel -r user delete 删用户及家目录
passwd password 改密码(-l-u 解锁 -e 强制改 -S 状态 -d 删密码)
chpasswd change password 批量改密码(读 user:pass 格式)
chage change age 密码有效期策略(-l-M 最长 -E 账号过期 -d 0 强制改)
chfn change full name 改 GECOS 描述字段
chsh change shell 改登录 shell(chsh -l 列出可用 shell)
id 查身份(-u -g -G -n
groups 查所属组
getent get entries 走 NSS 查询(LDAP/AD 环境下必用,比 grep 文件全)
who / w 当前登录用户 / 及其活动
last / lastb 登录历史 / 失败的登录尝试
lastlog 每个用户的最后登录时间
pwck / grpck check 检查 passwd/group 文件的完整性
vipw / vigr 安全地编辑 passwd/group(带锁和语法检查)
newusers 从文件批量创建用户

9.2 组管理命令

命令 作用
groupadd 建组(-g 指定 gid,-r 系统组)
groupmod 改组(-n 改名,-g 改 gid)
groupdel 删组
gpasswd -a/-d USER GROUP 加/删成员
gpasswd -M u1,u2 GROUP 设置成员列表(覆盖)
gpasswd -A USER GROUP 设置组管理员
newgrp GROUP 开新 shell 并切换主组
sg GROUP -c 'cmd' 以指定组身份执行一条命令

9.3 权限命令

命令 作用
chmod 改权限(数字法/符号法;-R 递归;大写 X 只给目录加 x
chown 改属主属组(-R 递归,-h 改软链接本身,--reference
chgrp 只改属组
umask 查看/设置默认权限掩码(-S 符号形式)
getfacl / setfacl ACL 查看/设置(-m-x-b 全删 -d 默认 -R 递归 -k 删默认)
namei -l 逐层显示路径权限(定位是哪一级挡住了)
stat -c '%a %U %G' 查看权限的八进制与属主
lsattr / chattr 文件属性(+i 不可修改,+a 只能追加)
getcap / setcap capabilities(比 SUID 精细,见第 23 篇)
sudo -l 列出自己被授权什么(到新环境第一件事)
visudo 安全编辑 sudoers(必须用它
sudoedit / sudo -e 提权编辑文件(编辑器不以 root 运行,比 sudo vim 安全)
sudoreplay 回放 sudo 录制的会话

9.4 chattr:比权限更硬的限制

sudo chattr +i /etc/resolv.conf         # i = immutable,任何人(含 root)都不能改
lsattr /etc/resolv.conf
# ----i---------e------- /etc/resolv.conf
sudo rm /etc/resolv.conf
# rm: cannot remove: Operation not permitted     <- root 也不行!
sudo chattr -i /etc/resolv.conf         # 先解除才能改

sudo chattr +a /var/log/audit.log       # a = append only,只能追加不能覆盖/删除
#                                          审计日志防篡改用它
sudo chattr +A file                     # A = 不更新 atime(性能)

# 实用场景
# ① 防止 NetworkManager/DHCP 覆盖你手改的 /etc/resolv.conf
# ② 保护关键日志不被入侵者清除
# ③ ⚠️ 排查「明明是 root 却改不了文件」时,记得 lsattr 一下

9.5 权限速查表

权限 数字 适用
rw------- 600 ssh 私钥、密码文件、含密钥的配置
rw-r----- 640 服务配置(root 写、服务账号读)
rw-r--r-- 644 普通文件的标准权限
rwx------ 700 私有目录、~/.ssh
rwxr-x--- 750 服务的数据/日志目录
rwxr-xr-x 755 可执行文件、公开目录的标准权限
rwxrwxr-x 775 组内可写的目录
rwxrwxrwt 1777 /tmp 类的公共可写目录(必须带 Sticky
rwxrwsr-x 2775 团队共享目录(SGID 继承属组)
rwsr-xr-x 4755 SUID 程序(尽量避免,改用 capabilities
rwxrwxrwx 777 ⚠️ 几乎永远是错的
# ssh 相关的权限要求(不对就会被 ssh 拒绝,且报错信息不明显)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519          # 私钥
chmod 644 ~/.ssh/id_ed25519.pub      # 公钥
chmod 600 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/known_hosts
# 家目录本身也不能是组/其他人可写的(否则 sshd 拒绝密钥登录)
chmod go-w ~

10. 面试题

Q:密码为什么不放在 /etc/passwd 而要单独放 /etc/shadow

因为 /etc/passwd 必须全局可读(644)——它承担「uid ↔ 用户名」的翻译职责,而 ls -lps aux 这类命令都需要做这个翻译,任何进程都得能读它。

如果密码哈希也在这个文件里,任何用户都能把它拷走做离线暴力破解。1980 年代确实如此,随着 CPU 变快成了严重漏洞。于是把哈希拆到 /etc/shadow(640,只有 root 和 shadow 组可读),/etc/passwd 的密码字段留一个 x 占位。

/etc/shadow 的第二个字段还承载了额外语义:!* 开头表示该账号无法用密码登录(被 passwd -l 锁定,或本来就是服务账号)。哈希前缀标明算法:$1$ MD5、$5$ SHA-256、$6$ SHA-512、$y$ yescrypt(现代默认,抗 GPU 破解)。

Q:usermod -G docker lhx 有什么问题?

-G 不加 -a 是「覆盖」语义——它会把用户从所有未在命令中列出的附加组里移除。如果 lhx 原本在 sudo 组,这条命令会把他踢出去;如果系统没有其他 root 途径(root 未设密码、没有其他 sudo 用户),你就把自己彻底锁在门外了。

正确写法是 usermod -aG docker lhx-a = append)。

第二个坑是改完组当前会话不生效:组成员关系是登录时读取并写入进程凭证的(/proc/<pid>/statusGroups: 行),内核不会自动刷新。所以「加了 docker 组还是 permission denied」几乎都是这个原因,解法是重新登录、newgrp docker、或 sg docker -c '命令'

Q:chmod 777 file 了为什么还是 Permission denied?

因为路径上每一级目录都需要 x(通行)权限。内核逐段解析路径,任何一级缺 x 就直接返回 EACCES,根本走不到文件本身——文件自己是 777 也没用。

namei -l /完整/路径 能逐层打印权限,一眼看出是哪一级挡住的。

延伸:目录的 rwx 与文件的完全不同。目录的 r 只能「列出名字」(ls 能看到名字但 ls -l 拿不到元数据,显示 -????????),x 才是「能否穿过去访问里面的东西」。只有 x 没有 r 时,知道确切文件名仍能访问——这正是家目录设 701 的用途:别人无法列出你有哪些文件,但 Web 服务器能通过确切路径读 ~/public_html/index.html

Q:为什么 chmod -R 755 /var/www 是危险的?正确做法是什么?

它会给所有普通文件(.html.jpg.conf)都加上执行权限。这不只是不整洁——某些 Web 服务器配置下可执行文件会被当作 CGI 执行,是实际的安全漏洞。

三种正确做法:

chmod -R u=rwX,go=rX /var/www        # ✅ 大写 X:只给目录和【本来就可执行】的文件加 x
find /var/www -type d -exec chmod 755 {} +      # ✅ 分别处理
find /var/www -type f -exec chmod 644 {} +
chmod --reference=/etc/hosts f       # ✅ 参照另一个文件

大写 X 就是为这个场景设计的,是很多人不知道的参数。

Q:umask 022 为什么新建文件是 644 而不是 755?

因为文件的基础权限是 666,不是 777。内核规定新建文件默认不带执行权限(避免数据文件被误当程序运行),所以 666 & ~022 = 644;而目录的基础权限是 777,777 & ~022 = 755

三个要点:① umask 是进程属性并被子进程继承,所以 cron 和 systemd 下的 umask 可能与交互式 shell 不同——这是「手动跑权限对、定时任务权限不对」的常见原因(systemd 里用 UMask=0027 显式设置);② umask 只影响新建文件;③ 想让文件可执行必须显式 chmod +x

Q:SUID 是什么?为什么 passwd 需要它?它有什么风险?

SUID 让可执行文件被任何人运行时,进程的有效 uid(euid)变成文件属主的 uidpasswd 需要写 /etc/shadow640 root:shadow,普通用户完全无写权限),但「用户改自己的密码」是合理需求,所以它被设为 SUID root:运行时 euid=0 能写 shadow,程序内部再检查「只能改自己的」。

风险在于它把「普通用户能触发的代码」和「root 权限」绑在一起,是提权漏洞的主要入口——一旦有溢出、命令注入、或允许用户指定任意路径/命令,就是直接的 root 提权。

三个加固手段:① 定期审计 find / -perm -4000 -type f 并与基线对比;② 给用户可写的分区加 nosuid 挂载选项;③ 自己的程序永远不要用 SUID,需要特权用 capabilitiessetcap cap_net_bind_service=+ep)或让 systemd 处理(AmbientCapabilities=)。

补充:SUID 对脚本无效(内核直接忽略),因为脚本的 SUID 有无法修补的竞态漏洞。

Q:/tmp 为什么需要 Sticky 位?怎么做一个团队共享目录?

/tmp 必须人人可写(777),但删除文件只看目录的 w 权限(因为删除是 unlink(),改的是目录里那一行记录),所以任何人都能删掉别人的临时文件。Sticky 位就是给这条规则打的补丁:目录带 Sticky 时,只有「文件属主」「目录属主」「root」能删除或重命名文件。

团队共享目录需要两件事配合:

  1. 目录设 SGIDchmod 2775 + chgrp team)—— 让新建文件的属组自动继承目录的属组,而不是创建者的主组
  2. 相关用户的 umask 设为 002 —— 否则新文件是 644,组成员只能读不能写

如果目录还允许所有人写(如上传目录),必须再加 Stickychmod 3775)防止互删。更精确的方案是用 ACL 的默认条目setfacl -d -m u:bob:rw dir),它能指定具体用户和具体权限位,不依赖 umask。

Q:为什么要配 sudoers 而不是共享 root 密码?

五个理由:审计(日志记录「谁、在哪、执行了什么」,而共享 root 时日志里全是 root)、最小权限(可以只授权特定命令)、易撤销(删一行配置 vs 改密码并通知所有人)、各用自己的密码(泄漏可追溯)、减少误操作(日常是普通用户,只在需要时提权)。

现代发行版(Ubuntu、macOS)默认根本不给 root 设密码(哈希是 !),一切通过 sudo。

必须用 visudo 编辑,因为它保存时做语法检查并加锁。一旦 /etc/sudoers 语法错误,sudo 会完全拒绝工作,没有其他 root 途径就只能进单用户模式修复。生产上应该用 /etc/sudoers.d/ 下的独立文件(权限必须 0440,文件名不能含点),便于配置管理工具幂等投放。

Q:给同事 sudo vim /etc/nginx/nginx.conf 的权限,安全吗?

完全不安全,等于给了完整 root。 vim 里可以 :!/bin/bash 直接开 root shell,或 :w /etc/sudoers 写任意文件、:r /etc/shadow 读任意文件。

同类的「可逃逸命令」很多:less/more/man!sh)、find-exec sh \;)、awkBEGIN{system("sh")})、tar--to-command)、git-c core.pager='!sh')、python/perl-c)、envdocker-v /:/host --privileged 直接拿宿主机 root)、以及任何能写任意文件的命令(写 /etc/sudoers~root/.ssh/authorized_keys/etc/cron.d/*)。这类知识有个专门的站点叫 GTFOBins

授权原则:只授权无法执行任意命令、也无法写任意路径的程序,比如 systemctl restart 具体服务名journalctl -u 具体服务。确实需要编辑配置就用 sudoeditsudo -e)——它把文件拷到临时位置、用普通用户权限启动编辑器、写回时才提权,所以 :!sh 只能得到普通 shell。

Q:susu -sudo -isudo -s 有什么区别?

用谁的密码 环境变量 cwd 审计
su root 的 保留当前 不变
su - root 的 重置为 root 的 /root
sudo -s 自己的 保留当前 不变 ✅ 强
sudo -i 自己的 重置为 root 的 /root ✅ 强

-(或 -i)的含义是模拟完整登录:读目标用户的 profile、重置环境变量、切到其家目录。

推荐 sudo -i:审计完整,且环境干净——避免继承你自己的 PATHLD_PRELOAD 等变量导致意外行为。这也解释了「为什么 sudo go version 报 command not found」:sudo 用的是 sudoers 里的 secure_path 而不是你的 PATH,这是防提权设计(防止你用篡改的 PATH 让 sudo 执行伪造的程序)。

Q:怎么给一个 Go 服务配置最小权限?

六步:

  1. 专用系统账号useradd -r -s /usr/sbin/nologin -M myapp(uid < 1000、不能登录、无家目录)。不用 root 跑服务是因为一旦被 RCE,攻击者直接拿到 root
  2. 按 FHS 建目录并设权限install -d -m 750 -o myapp -g myapp /var/lib/myapp,配置用 640(root 写、服务组读)。
  3. 需要绑低端口不要用 root:用 setcap cap_net_bind_service=+ep,或 systemd 的 AmbientCapabilities=CAP_NET_BIND_SERVICE,或前面挂 nginx 反代。
  4. systemd 沙箱加固NoNewPrivilegesProtectSystem=strictPrivateTmpProtectKernelTunablesRestrictSUIDSGIDSystemCallFilter=@system-serviceReadWritePaths= 显式白名单。用 systemd-analyze security myapp 看评分。
  5. 目录交给 systemd 管RuntimeDirectory=/StateDirectory=/LogsDirectory= 自动创建并设属主。
  6. 部署账号最小 sudo:只授权 systemctl restart 具体服务,不给 systemctl 通用权限。

原则是最小权限 + 纵深防御 + 可审计:即使服务被攻破,还有 sandbox 挡着;一切操作有日志。


小结

  • 内核只认 uid/gid 数字,用户名是翻译层。root 的特权只是「uid == 0」
  • 密码必须从 /etc/passwd 挪到 /etc/shadow,因为前者必须全局可读(uid↔名字的翻译人人需要)
  • usermod -G 不加 -a 会踢掉所有附加组 —— 可能把自己锁在门外。改组后当前会话不生效,因为凭证在登录时快照
  • 目录的 x 是通行权,路径上任何一级缺 x 都走不通(用 namei -l 定位)。r 只能列名字
  • 删除文件看的是目录的 w —— 所以才需要 Sticky 位打补丁
  • chmod -R 755 会把所有文件变可执行,用大写 Xfind -type 分别处理
  • umask 的基础是文件 666、目录 777(文件默认不带 x);它是进程属性,cron/systemd 下可能不同
  • SUID 是提权漏洞的主要入口,自己的程序改用 capabilities;给用户可写分区加 nosuid
  • sudo vim 等于完整 root:!sh),授权只给「不能执行任意命令、不能写任意路径」的程序,编辑配置用 sudoedit
  • 推荐 sudo -i:审计完整、环境干净
  • Go 服务的最小权限落地:专用系统账号 + capabilities + systemd 沙箱 + 精确 sudo 授权

下一篇讲 磁盘、分区与挂载lsblk/df/du 的分工、分区表 MBR 与 GPT 的区别、mount 的常用选项都在解决什么问题、/etc/fstab 六个字段为什么这样设计、UUID 为什么比设备名可靠、LVM 的基本概念,以及「磁盘满了但 du 找不到」的完整排查路径。