目录
Please enable Javascript to view the contents

Linux查漏补缺-2-特殊权限[s·t]

 ·  ☕ 9 分钟
核心结论细节
SUID 改变进程有效用户普通用户执行带 SUID 的二进制程序时,进程临时获得该程序属主权限,典型例子是 /usr/bin/passwd
SGID 对文件和目录含义不同用在可执行文件上改变有效组;用在目录上让新建文件继承目录属组
Sticky Bit 主要用于公共可写目录目录可写不代表可以删除别人文件,带 t 后只有文件属主、目录属主、root 可以删除或重命名
1777777 更安全1777 没有增加写权限,而是在 777 基础上收缩删除/重命名权限
大写 S/T 通常表示缺少执行位rwSrwT 说明特殊权限位存在,但对应 x 位不存在,通常不是预期结果

1.简介

在 Kubernetes 中部署 MariaDB 时,挂载 local-storagebinlog 目录曾出现权限不足。目录已设置 777,后续通过 chmod 1777 binlog/ 处理。

从权限模型看,1777 不会比 777 增加写权限,只是额外开启 Sticky Bit。因此这类问题不能只看 rwx,还需要区分特殊权限、运行用户、UID/GID、挂载参数、SELinux/AppArmor 等因素。

这类问题容易暴露一个误区:Linux 权限不只有 rwx,还存在三类特殊权限:

  • SUID
  • SGID
  • Sticky Bit,也就是常见的 t 权限

需要注意的是,1777 中真正改变的是 Sticky Bit,它主要限制公共目录中文件的删除行为,不负责解决所有写入权限问题。

2.说明

2.1 特殊权限总览

权限数字位显示位置常见对象核心作用
SUID4用户执行位可执行二进制文件执行进程临时拥有文件属主权限
SGID2组执行位可执行二进制文件、目录文件:进程临时拥有文件属组权限;目录:新文件继承目录属组
Sticky Bit1其他用户执行位目录公共可写目录中,限制删除/重命名他人文件

权限显示位置如下:

普通权限特殊权限说明
-rwxr-xr-x-rwsr-xr-xSUID,占用用户执行位
-rwxr-xr-x-rwxr-sr-xSGID,占用组执行位
drwxrwxrwxdrwxrwxrwtSticky Bit,占用其他用户执行位
特殊权限不是新增一列权限,而是复用了 user/group/other 的执行位显示位置。因此 s/tx 的关系必须一起看。

2.2 SUID:执行程序时临时获得文件属主权限

SUID 全称是 Set UID

它的作用是:普通用户执行某个带 SUID 的可执行程序时,该进程的有效用户 ID 会变成这个程序的属主。

典型例子是 /usr/bin/passwd

普通用户没有权限直接写 /etc/shadow

1
2
3
$ stat /etc/shadow
  File: /etc/shadow
Access: (0640/-rw-r-----)  Uid: (    0/    root)   Gid: (   42/  shadow)

但普通用户可以执行 passwd 修改自己的密码,因为 /usr/bin/passwd 带 SUID:

1
2
3
$ stat /usr/bin/passwd
  File: /usr/bin/passwd
Access: (4755/-rwsr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)

关键点:

  1. passwd 属主是 root
  2. 权限是 4755,用户执行位显示为 s
  3. 普通用户执行 passwd 时,进程临时具备 root 权限;
  4. passwd 程序内部再按规则修改 /etc/shadow
SUID 不是让用户永久变成 root,而是让这个程序的执行进程在运行期间临时拥有文件属主权限。

安全风险:

  • SUID 程序如果属主是 root,相当于给普通用户开放了一条受控提权路径;
  • 如果程序存在命令注入、路径劫持、越权读写等问题,容易变成提权漏洞;
  • 不建议随意给自写程序设置 SUID root。

限制:

  • SUID 主要用于可执行二进制文件;
  • 不建议依赖 SUID shell 脚本实现提权,Linux 出于安全原因通常会忽略脚本文件上的 SUID 位;
  • 脚本内部调用的命令是否提权,取决于被调用的二进制程序自身权限,而不是脚本文件权限。

2.3 SGID:文件看执行组,目录看继承组

SGID 全称是 Set GID

它有两种常见用法。

2.3.1 用在可执行文件上

如果 SGID 设置在可执行文件上,用户执行该程序时,进程的有效组会变成该文件的属组。

典型例子是 plocate

1
2
3
$ ls -l /usr/bin/plocate /var/lib/plocate/plocate.db
-rwxr-sr-x 1 root plocate   313904 Feb 17  2022 /usr/bin/plocate
-rw-r----- 1 root plocate 70159347 Oct 26 19:14 /var/lib/plocate/plocate.db

说明:

  1. /var/lib/plocate/plocate.db 属组是 plocate,普通用户不能直接读取;
  2. /usr/bin/plocate 带 SGID,属组也是 plocate
  3. 普通用户执行 plocate 时,进程临时获得 plocate 组权限;
  4. 因此程序可以读取 plocate.db

2.3.2 用在目录上

如果 SGID 设置在目录上,用户在该目录下新建文件时,文件属组会继承目录属组,而不是用户当前默认组。

适用场景:多人协作目录。

1
2
3
mkdir /data/project
chgrp dev /data/project
chmod 2775 /data/project

效果:

1
2
$ ls -ld /data/project
drwxrwsr-x 2 root dev 4096 Oct 26 19:00 /data/project

后续任何有写权限的用户在 /data/project 下创建文件,新文件属组都会是 dev

常见用途:

  • 团队共享目录;
  • 应用日志目录;
  • 需要保持统一属组的构建产物目录。

2.4 Sticky Bit:公共目录中只能删除自己的文件

Sticky Bit 主要用于目录,对普通文件基本没有实际意义。

它解决的问题是:目录对所有人可写时,如何防止用户删除别人的文件。

典型例子是 /tmp

1
2
$ ls -ld /tmp
drwxrwxrwt 40 root root 4096 Oct 26 17:29 /tmp

/tmp1777

  • 777:所有用户都可以进入、创建、修改自己有权限的文件;
  • 1:开启 Sticky Bit;
  • t:显示在 other 的执行位上。

开启 Sticky Bit 后,目录中的文件只能由以下用户删除或重命名:

  1. 文件属主;
  2. 目录属主;
  3. root。

验证示例:

1
2
3
4
sudo mkdir /tmp/sticky-test
sudo chmod 1777 /tmp/sticky-test
sudo touch /tmp/sticky-test/root-file
sudo chmod 666 /tmp/sticky-test/root-file

普通用户尝试删除 root 创建的文件:

1
2
$ rm /tmp/sticky-test/root-file
rm: cannot remove '/tmp/sticky-test/root-file': Operation not permitted

即使文件本身是 666,由于目录启用了 Sticky Bit,普通用户也不能删除不属于自己的文件。

2.5 设置和移除特殊权限

数字方式是在普通三位权限前再加一位:

数字含义常见程度
0无特殊权限默认
1Sticky Bit常见,典型如 /tmp1777
2SGID常见,典型如共享目录的 2775
3SGID + Sticky Bit较少,用于既要继承属组、又要限制删除的共享目录
4SUID常见,典型如 /usr/bin/passwd4755
5SUID + Sticky Bit罕见,实际很少使用
6SUID + SGID较少,同时改变有效用户和有效组
7SUID + SGID + Sticky Bit罕见,不建议无明确原因使用

常用命令:

操作数字方式符号方式
设置 SUIDchmod 4755 <file>chmod u+s <file>
移除 SUID-chmod u-s <file>
设置 SGID 文件chmod 2755 <file>chmod g+s <file>
设置 SGID 目录chmod 2775 <dir>chmod g+s <dir>
移除 SGID-chmod g-s <file-or-dir>
设置 Sticky Bitchmod 1777 <dir>chmod o+t <dir>
移除 Sticky Bit-chmod o-t <dir>

举例:

1
2
3
chmod 1777 /tmp/app-cache
chmod 2775 /data/project
chmod u+s /usr/local/bin/example

不建议对不明确来源的程序执行:

1
chmod 4755 unknown-bin

2.6 大写 S 与大写 T

chmod 不会判断特殊权限是否合理。即使没有执行位,也可以设置 s/t

示例:

1
2
3
touch test
chmod 4664 test
ls -l test

结果:

1
-rwSrw-r-- 1 hex hex 0 Oct 26 18:53 test

S 为大写,原因是用户执行位没有 x

规则如下:

显示条件说明
sSUID/SGID + 对应执行位存在特殊权限位存在,执行位也存在
SSUID/SGID 存在,但对应执行位不存在通常不是预期结果
tSticky Bit + other 执行位存在公共目录常见显示
TSticky Bit 存在,但 other 执行位不存在目录无法被 other 进入,通常不是预期结果

总结:

  • 小写 s/t:对应 x 位存在;
  • 大写 S/T:对应 x 位不存在;
  • 大写通常表示权限设置不完整,需要重点检查。

2.7 K8s / Docker 场景:为什么 MariaDB 卷挂载会遇到权限问题

容器挂载宿主机目录时,权限判断仍然发生在 Linux 文件系统层面。常见问题不是 MariaDB 特有,而是容器 UID/GID 与宿主机目录权限不匹配。

以 MariaDB 为例:

  1. 容器内数据库进程通常不是 root,而是 mysql 用户;
  2. 宿主机目录经常由 root 创建,例如 root:root
  3. MariaDB 数据目录、binlog 目录需要创建文件、重命名文件、轮转日志、清理旧 binlog;
  4. 如果目录属主、属组、SELinux/AppArmor、挂载参数不匹配,就会出现 Permission denied、初始化失败、binlog 创建失败等问题。

这里需要区分两件事:

权限能力说明
777所有人可读、可写、可进入目录允许任意用户创建、删除、重命名目录中的文件
1777所有人可读、可写、可进入目录,但删除受限只有文件属主、目录属主、root 可以删除或重命名文件
1777 不是比 777 放大权限,而是在 777 的基础上开启 Sticky Bit,收缩删除/重命名权限。它更安全,不是更宽松。

对于 MariaDB binlog 目录,1777 的价值主要在保护删除行为:

  • MariaDB 可以创建、轮转、清理自己生成的 binlog;
  • 其他同样具备目录写权限的进程,不能随意删除 MariaDB 创建的 binlog;
  • 多容器、initContainer、运维脚本共用挂载目录时,可以降低误删风险。

需要注意的是,如果问题是“无法写入”,1777 本身不比 777 多提供写权限。真正需要检查的是:

  • 容器进程运行 UID/GID;
  • 宿主机目录属主和属组;
  • K8s securityContext
  • 挂载是否只读;
  • SELinux/AppArmor 是否拦截。
K8s 中更推荐的处理方式:runAsUser / runAsGroup / fsGroup / initContainer

方式一:显式声明运行用户和卷属组

Kubernetes 官方推荐通过 securityContext 控制容器进程身份。常用字段:

字段作用
runAsUser指定容器主进程使用哪个 UID 运行
runAsGroup指定容器主进程使用哪个 GID 运行
fsGroup挂载卷中文件系统对象补充属组,便于进程通过组权限访问卷

关键配置示例,UID/GID 需要以镜像内 id mysql 的实际结果为准:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mariadb
spec:
  serviceName: mariadb
  selector:
    matchLabels:
      app: mariadb
  template:
    metadata:
      labels:
        app: mariadb
    spec:
      securityContext:
        runAsUser: 999
        runAsGroup: 999
        fsGroup: 999
      containers:
        - name: mariadb
          image: mariadb:10.11
          volumeMounts:
            - name: data
              mountPath: /var/lib/mysql
            - name: binlog
              mountPath: /var/log/mysql
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 20Gi
    - metadata:
        name: binlog
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 10Gi

这种方式比直接 chmod 777 更适合生产环境。

方式二:initContainer 初始化目录权限

如果使用 hostPathlocal-storage,宿主机目录可能提前由 root 创建。可以用 initContainer 在数据库启动前修正目录权限。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
initContainers:
  - name: init-permissions
    image: busybox:1.36
    command:
      - sh
      - -c
      - |
        chown -R 999:999 /var/lib/mysql /var/log/mysql
        chmod 770 /var/lib/mysql
        chmod 1777 /var/log/mysql        
    securityContext:
      runAsUser: 0
    volumeMounts:
      - name: data
        mountPath: /var/lib/mysql
      - name: binlog
        mountPath: /var/log/mysql

说明:

  • 数据目录建议使用 770750,不要无脑 777
  • binlog 如果是共享可写目录,可以使用 1777 收缩删除权限;
  • 如果目录只被 MariaDB 单实例使用,更推荐 chown mysql:mysql + 750/770
  • hostPath 依赖宿主机路径和权限,不建议作为数据库生产存储首选。

方式三:排查是否为 SELinux / 只读挂载

如果 chmod 777 后仍然报 Permission denied,需要确认不是 Linux DAC 权限之外的问题。

1
2
3
4
kubectl describe pod <pod-name>
kubectl exec -it <pod-name> -- id
kubectl exec -it <pod-name> -- ls -ld /var/lib/mysql /var/log/mysql
kubectl exec -it <pod-name> -- touch /var/log/mysql/test-write

重点检查:

  • volumeMounts.readOnly 是否为 true
  • PV/PVC 是否只读挂载;
  • 节点是否启用 SELinux;
  • 容器是否被 AppArmor/seccomp 限制;
  • 存储插件是否支持 fsGroup
Docker run / docker compose 场景

Docker bind mount 会把宿主机目录直接挂进容器。目录属主、属组、权限仍以宿主机文件系统为准。

1
2
3
4
mkdir -p /data/mariadb /data/mariadb-binlog
chown -R 999:999 /data/mariadb
chmod 770 /data/mariadb
chmod 1777 /data/mariadb-binlog

启动示例:

1
2
3
4
5
6
7
docker run -d \
  --name mariadb \
  --user 999:999 \
  -v /data/mariadb:/var/lib/mysql \
  -v /data/mariadb-binlog:/var/log/mysql \
  -e MARIADB_ROOT_PASSWORD='example' \
  mariadb:10.11

如果宿主机启用 SELinux,Docker bind mount 还可能需要 :z:Z

1
2
3
4
docker run -d \
  --name mariadb \
  -v /data/mariadb:/var/lib/mysql:Z \
  mariadb:10.11

需要注意:

  • :z 表示该目录可被多个容器共享;
  • :Z 表示该目录私有给当前容器;
  • 不要对系统关键目录随意使用 :Z,可能影响宿主机文件标签。

2.8 对比表

对比项SUIDSGIDSticky Bit
数字位421
常见权限47552755 / 27751777
显示样例-rwsr-xr-x-rwxr-sr-x / drwxrwsr-xdrwxrwxrwt
常见对象可执行二进制文件可执行二进制文件、目录目录
文件效果执行时使用文件属主身份执行时使用文件属组身份基本无意义
目录效果通常无意义新建文件继承目录属组限制删除/重命名他人文件
典型场景/usr/bin/passwd共享目录、plocate/tmp、公共缓存目录
风险点提权风险最高组权限扩大误以为解决读写权限,实际只限制删除

3.总结

  1. SUID 用于可执行二进制文件,核心是让进程临时拥有文件属主权限,典型场景是普通用户通过 passwd 修改自己的密码;
  2. SGID 用在文件上表示执行进程临时拥有文件属组权限,用在目录上表示新建文件继承目录属组;
  3. Sticky Bit 主要用于公共可写目录,目录中只有文件属主、目录属主、root 可以删除或重命名文件;
  4. chmod 1777 <dir> 中的 1 不是增加写权限,而是在 777 基础上收缩删除/重命名权限,因此比 777 更安全;
  5. K8s / Docker 挂载 MariaDB 卷时,优先检查 UID/GID、fsGrouprunAsUser、挂载只读、SELinux/AppArmor,不建议把 777 当成长期方案;
  6. 看到大写 S/T 时,需要检查对应执行位是否缺失,这通常不是预期权限配置。

4.参考

[注1]: 鸟哥-特殊权限SUID说明

[注2]: 鸟哥-常见指令需要的基本权限

分享

Hex
作者
Hex
CloudNative Developer