Linux-10 用户、组与权限实操:从 useradd 到 sudoers 的每一步
这一篇讲操作:怎么建用户、怎么配组、怎么给权限、怎么配 sudo。每个操作都会顺带说清「为什么是这样」,但完整的设计演进史(DAC 的缺陷、capabilities 如何拆解 root、SELinux 为什么要在 DAC 之上再加一层)留给第 23 篇。
先看五个问题:
useradd和adduser有什么区别?为什么会有两个命令?- 密码为什么不存在
/etc/passwd里,非要挪到/etc/shadow? chmod 777给了最大权限,为什么还是Permission denied?- 为什么要配
sudoers,而不是直接把 root 密码告诉同事? su、su -、sudo -i、sudo -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 | 传统的静态系统账号(bin、daemon、mail) |
| 100~999 | 动态分配的系统/服务账号(nginx、mysql、systemd-*) |
| 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/useradd 的 SHELL=(常是 /bin/sh) |
/bin/bash |
| 可用性 | 所有发行版都有(POSIX 工具链) | 只有 Debian/Ubuntu 有(RHEL 上 adduser 是 useradd 的软链接) |
| 适用 | 脚本、自动化 | 手动交互建账号 |
# 手动建人类用户: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 做两件关键的事:
- 保存时做语法检查,语法错误会提示你重新编辑而不是直接写坏文件
- 加锁,防止两个人同时编辑
一旦 /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:审计完整,且环境干净(避免继承你自己的 PATH、LD_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 # 验证授权
这套流程体现的三条原则:
- 最小权限:专用账号 + 只给必需的 capability + systemd 沙箱
- 纵深防御:即使服务被攻破,还有
ProtectSystem、NoNewPrivileges、SystemCallFilter挡着 - 可审计:一切通过 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 -l、ps 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>/status 的 Groups: 行),内核不会自动刷新。所以「加了 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)变成文件属主的 uid。passwd 需要写 /etc/shadow(640 root:shadow,普通用户完全无写权限),但「用户改自己的密码」是合理需求,所以它被设为 SUID root:运行时 euid=0 能写 shadow,程序内部再检查「只能改自己的」。
风险在于它把「普通用户能触发的代码」和「root 权限」绑在一起,是提权漏洞的主要入口——一旦有溢出、命令注入、或允许用户指定任意路径/命令,就是直接的 root 提权。
三个加固手段:① 定期审计 find / -perm -4000 -type f 并与基线对比;② 给用户可写的分区加 nosuid 挂载选项;③ 自己的程序永远不要用 SUID,需要特权用 capabilities(setcap cap_net_bind_service=+ep)或让 systemd 处理(AmbientCapabilities=)。
补充:SUID 对脚本无效(内核直接忽略),因为脚本的 SUID 有无法修补的竞态漏洞。
Q:/tmp 为什么需要 Sticky 位?怎么做一个团队共享目录?
/tmp 必须人人可写(777),但删除文件只看目录的 w 权限(因为删除是 unlink(),改的是目录里那一行记录),所以任何人都能删掉别人的临时文件。Sticky 位就是给这条规则打的补丁:目录带 Sticky 时,只有「文件属主」「目录属主」「root」能删除或重命名文件。
团队共享目录需要两件事配合:
- 目录设 SGID(
chmod 2775+chgrp team)—— 让新建文件的属组自动继承目录的属组,而不是创建者的主组 - 相关用户的 umask 设为 002 —— 否则新文件是 644,组成员只能读不能写
如果目录还允许所有人写(如上传目录),必须再加 Sticky(chmod 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 \;)、awk(BEGIN{system("sh")})、tar(--to-command)、git(-c core.pager='!sh')、python/perl(-c)、env、docker(-v /:/host --privileged 直接拿宿主机 root)、以及任何能写任意文件的命令(写 /etc/sudoers、~root/.ssh/authorized_keys、/etc/cron.d/*)。这类知识有个专门的站点叫 GTFOBins。
授权原则:只授权无法执行任意命令、也无法写任意路径的程序,比如 systemctl restart 具体服务名、journalctl -u 具体服务。确实需要编辑配置就用 sudoedit(sudo -e)——它把文件拷到临时位置、用普通用户权限启动编辑器、写回时才提权,所以 :!sh 只能得到普通 shell。
Q:su、su -、sudo -i、sudo -s 有什么区别?
| 用谁的密码 | 环境变量 | cwd | 审计 | |
|---|---|---|---|---|
su |
root 的 | 保留当前 | 不变 | 弱 |
su - |
root 的 | 重置为 root 的 | /root |
弱 |
sudo -s |
自己的 | 保留当前 | 不变 | ✅ 强 |
sudo -i |
自己的 | 重置为 root 的 | /root |
✅ 强 |
-(或 -i)的含义是模拟完整登录:读目标用户的 profile、重置环境变量、切到其家目录。
推荐 sudo -i:审计完整,且环境干净——避免继承你自己的 PATH、LD_PRELOAD 等变量导致意外行为。这也解释了「为什么 sudo go version 报 command not found」:sudo 用的是 sudoers 里的 secure_path 而不是你的 PATH,这是防提权设计(防止你用篡改的 PATH 让 sudo 执行伪造的程序)。
Q:怎么给一个 Go 服务配置最小权限?
六步:
- 专用系统账号:
useradd -r -s /usr/sbin/nologin -M myapp(uid < 1000、不能登录、无家目录)。不用 root 跑服务是因为一旦被 RCE,攻击者直接拿到 root。 - 按 FHS 建目录并设权限:
install -d -m 750 -o myapp -g myapp /var/lib/myapp,配置用 640(root 写、服务组读)。 - 需要绑低端口不要用 root:用
setcap cap_net_bind_service=+ep,或 systemd 的AmbientCapabilities=CAP_NET_BIND_SERVICE,或前面挂 nginx 反代。 - systemd 沙箱加固:
NoNewPrivileges、ProtectSystem=strict、PrivateTmp、ProtectKernelTunables、RestrictSUIDSGID、SystemCallFilter=@system-service、ReadWritePaths=显式白名单。用systemd-analyze security myapp看评分。 - 目录交给 systemd 管:
RuntimeDirectory=/StateDirectory=/LogsDirectory=自动创建并设属主。 - 部署账号最小 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会把所有文件变可执行,用大写X或find -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 找不到」的完整排查路径。
xingliuhua