Linux 基础体系 · 第 74/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

AppArmor 完整基础:Profile、Mode、规则、日志和服务加固

AppArmor 是 Linux 内核支持的一种强制访问控制(Mandatory Access Control,MAC)机制。它通过为进程绑定安全策略,限制该进程能够访问的文件、使用的能力、建立的网络连接、执行的程序以及与其他进程交互的方式。

AppArmor 的核心不是“给文件加一个安全标签”,而是回答:

当某个进程以某个 Profile 运行时,它是否被允许执行某个具体操作?

例如:

  • /usr/sbin/nginx 是否可以读取 /etc/nginx/nginx.conf
  • 一个 Web 服务是否可以写入 /var/www/html/
  • sshd 是否可以执行用户登录后的 shell?
  • 一个进程是否可以使用 CAP_SYS_ADMIN
  • 一个服务是否可以向另一个进程发送信号?
  • 一个程序是否可以执行 /bin/sh,以及执行后是否仍然处于原 Profile 的约束下?

AppArmor 适合做进程级服务加固,尤其适合约束文件访问边界、限制服务执行额外程序,并为生产环境提供拒绝日志和审计线索。


一、AppArmor 解决什么问题

1. DAC 权限不是完整的服务安全边界

Linux 传统权限模型主要是自主访问控制(Discretionary Access Control,DAC):

用户 UID/GID
    +
文件 owner/group/mode
    +
ACL

例如:

-rw-r----- 1 root nginx /etc/app.conf

如果进程以 nginx 用户运行,它通常只能根据文件的 owner、group 和 mode 判断是否允许访问。

但仅靠 DAC 存在几个问题:

  1. 服务可能被配置为 root 运行;
  2. 服务进程可能被利用后继承大量用户权限;
  3. 文件权限通常难以精确表达“某个程序可以读,但不能写”;
  4. 同一个用户启动的多个程序无法通过普通 DAC 区分;
  5. 服务可能拥有不必要的能力,例如绑定低端口、加载内核模块或修改系统时间。

AppArmor 在 DAC 之后增加一道约束。一个访问通常需要同时满足:

DAC 允许访问
∧
AppArmor Profile 允许访问

如果 DAC 允许但 AppArmor 拒绝,最终仍然拒绝;如果 AppArmor 允许但 DAC 拒绝,最终也仍然拒绝。

因此 AppArmor 通常不会替代文件权限,而是缩小已经存在的权限。

2. AppArmor 的基本安全模型

定义:

  • p:当前进程;
  • P(p):进程当前绑定的 Profile;
  • o:请求执行的操作;
  • r:访问的资源,例如路径、能力、网络族;
  • DAC(p, o, r):传统 DAC 是否允许;
  • MAC(P(p), o, r):Profile 是否允许。

最终允许条件可以写成:

Allow(p, o, r) =
    DAC(p, o, r) = true
    ∧
    MAC(P(p), o, r) = true

这个公式说明了三个常见误解:

  • AppArmor 不能让普通用户读取原本没有权限读取的文件;
  • 给 Profile 增加 r 规则不会绕过文件 mode;
  • 删除或禁用 Profile 可能扩大权限,但不会自动改变文件本身的 DAC 权限。

AppArmor 的价值在于:即使攻击者控制了某个服务进程,也只能在该 Profile 允许的范围内继续行动。


二、AppArmor 的组件和数据流

一个典型的 AppArmor 系统包含以下部分:

flowchart LR
    A[Profile 文件] --> B[apparmor_parser]
    B --> C[内核 LSM 策略]
    D[进程启动或执行] --> E[Profile 匹配/转换]
    E --> C
    F[文件/网络/能力/信号操作] --> G[内核 AppArmor Hook]
    C --> G
    G --> H{规则允许?}
    H -- 是 --> I[继续执行]
    H -- 否 --> J[拒绝或记录]
    J --> K[内核审计日志]
    K --> L[journald/auditd]
    L --> M[aa-logprof 等分析工具]

关键路径是:

  1. Profile 以文本形式存在于系统目录中;
  2. apparmor_parser 将 Profile 编译并加载到内核;
  3. 进程执行文件时,内核根据 Profile 的附着条件决定是否绑定 Profile;
  4. 进程访问文件、使用能力或执行其他程序时,内核中的 LSM 检查点介入;
  5. 如果规则不允许,则根据当前 Mode 拒绝、记录或终止操作;
  6. 拒绝事件通常进入内核审计通道,再由 journaldauditd 收集。

AppArmor Profile 是内核状态,不是“只要文件存在就自动生效”的普通配置文件。修改 Profile 文件后,必须重新加载,已经运行的进程也需要确认是否获得了新的策略状态。


三、Profile:AppArmor 的策略单位

1. Profile 的定义

Profile 是一组绑定到程序或执行路径的安全规则。例如:

#include <tunables/global>

/usr/sbin/example-service {
    /etc/example-service/config.conf r,
    /var/lib/example-service/** rw,
    /var/log/example-service/*.log w,
}

这个 Profile 表示:当程序 /usr/sbin/example-service 在该 Profile 下运行时,允许读取配置文件、读写自己的状态目录,并写入特定日志文件。

Profile 的名称通常与附着路径相同,但二者不是绝对等价的概念。Profile 可以通过执行路径匹配程序,也可以通过其他方式被显式指定或由父 Profile 转换。

2. Profile 的附着与执行转换

进程启动时,AppArmor 需要决定:

  • 是否为该进程绑定 Profile;
  • 绑定哪个 Profile;
  • 子进程执行新程序后,是继承旧 Profile,还是切换到新 Profile;
  • 新程序没有匹配 Profile 时,是否继续在原 Profile 中运行。

最常见的执行规则形式如下:

/usr/bin/helper ix,
/usr/bin/parser px -> parser-profile,

其中:

  • ix:执行目标程序,但继承当前 Profile;
  • px:执行目标程序并切换到指定 Profile;
  • px -> parser-profile:明确切换到名为 parser-profile 的 Profile。

一个简化的状态变化如下:

服务进程 P,当前 Profile = service-profile
        |
        | execve("/usr/bin/helper")
        | 规则为 ix
        v
helper 进程,Profile = service-profile

服务进程 P,当前 Profile = service-profile
        |
        | execve("/usr/bin/parser")
        | 规则为 px -> parser-profile
        v
parser 进程,Profile = parser-profile

ix 适合那些应该完全继承父进程限制的辅助程序。px 适合为不同子程序建立更窄或不同的权限边界。

3. 继承、切换和不受限执行的风险

规则中的执行权限不能只理解成“能否执行文件”。执行还决定了进程后续使用哪个 Profile。

常见执行模式包括:

模式 含义
ix 执行并继承当前 Profile
px 执行并切换到指定 Profile
cx 执行并切换到子 Profile
ux 执行后转为 unconfined,不受 AppArmor 约束
x 允许执行,但通常需要与具体转换语义结合

ux 是高风险能力。它可能成为服务逃逸路径:

受限服务
    |
    | 执行 /bin/sh,规则为 ux
    v
不受 AppArmor 约束的 shell

这并不意味着 shell 自动拥有 root 权限,但它失去了当前 Profile 的 MAC 限制。如果服务本身以高权限运行,风险会非常大。

在生产 Profile 中,通常优先使用 ix 或显式的 px,而不是允许 ux


四、Profile Mode:Enforce、Complain 和 Kill

Mode 决定了 Profile 违反规则时的行为。它不改变规则本身,而改变违规事件的处理方式。

1. Enforce Mode

Enforce 是正常的强制模式:

请求操作
    |
    | Profile 允许
    v
操作继续

请求操作
    |
    | Profile 拒绝
    v
操作失败 + 记录审计事件

常见表现是:

  • 文件打开返回 Permission denied
  • 执行程序失败;
  • 网络或能力操作返回错误;
  • 日志中出现 apparmor="DENIED"
  • 应用程序可能因此启动失败、无法加载配置或无法写入状态文件。

查询状态:

sudo aa-status

常见输出会包含:

apparmor module is loaded.
N profiles are loaded.
M profiles are in enforce mode.
K profiles are in complain mode.

具体数量和格式因发行版、AppArmor 工具版本而异,不应将输出行数当作固定接口。

2. Complain Mode

Complain 模式通常允许原本会被拒绝的操作,但记录审计事件:

请求操作
    |
    | Profile 规则不允许
    v
操作仍然执行 + 记录事件

它适合:

  • 初次为已有服务建立 Profile;
  • 观察服务的正常工作集;
  • 识别缺少的路径、能力和执行转换;
  • 在不立即中断业务的前提下收集证据。

切换命令:

sudo aa-complain /etc/apparmor.d/usr.sbin.example-service

切换到强制模式:

sudo aa-enforce /etc/apparmor.d/usr.sbin.example-service

Complain 模式不是“安全模式”。因为违规操作仍然可能成功,所以攻击者在此期间可以访问或修改 Profile 原本想保护的资源。

此外,Complain 模式采集的是“实际走到的行为”,不是完整的业务权限证明。如果某个异常路径、定时任务、故障恢复分支或升级流程没有在测试期间运行,日志中也不会出现对应需求。

3. Kill Mode

某些 AppArmor 实现和 Profile 支持 kill 相关语义:发生违反时终止触发违规的任务,而不仅仅是返回访问失败。

这比 Enforce 更激进,可能用于高风险服务,但需要特别确认:

  • 当前内核和 AppArmor 工具版本是否支持目标语法;
  • 终止的是哪个任务;
  • 服务管理器是否会自动重启;
  • 重启循环是否会造成可用性问题;
  • 应用是否会因为一次误判导致整个业务实例退出。

不要在没有验证内核、工具链和恢复流程的情况下直接对生产服务启用 Kill 行为。

4. unconfined 不是一种安全的 Profile Mode

aa-status 中经常会看到 unconfined 进程。它表示该进程当前没有受到 AppArmor Profile 的约束,通常不是“一个允许所有操作的安全 Profile”。

需要区分:

enforce      有 Profile,违规被拒绝
complain     有 Profile,违规通常只记录
unconfined   没有有效 Profile 约束

一个进程即使运行在 unconfined 状态,也仍然受到 DAC、其他 LSM、seccomp、容器边界和内核权限检查的影响;但它失去了 AppArmor 这一层控制。


五、规则的基本结构

一个常见 Profile 的结构如下:

#include <tunables/global>

/usr/local/sbin/example-service {
    #include <abstractions/base>

    /etc/example-service/config.yaml r,
    /etc/example-service/secret.key r,

    /var/lib/example-service/** rw,
    /var/log/example-service/*.log w,

    /usr/bin/helper ix,
}

结构可以分为:

  1. 预处理和变量定义;
  2. Profile 名称及附着条件;
  3. include 的通用规则;
  4. 文件规则;
  5. 能力、网络、信号、执行等规则;
  6. Profile 结束的大括号。

规则以逗号结束。注释使用 #


六、文件路径规则

1. 基本文件权限

AppArmor 常见文件规则权限包括:

权限 含义
r 读取文件
w 写入文件
a 追加写入
k 文件锁
l 创建硬链接
m 可执行内存映射,常与动态库或执行文件有关
x 执行
ix 执行并继承当前 Profile
px 执行并切换到另一个 Profile

示例:

/etc/example/config.yaml r,
/var/lib/example/data.db rw,
/var/log/example/access.log w,
/usr/bin/helper ix,

这些规则的含义分别是:

  • 配置文件只能读;
  • 数据库文件可以读写;
  • 日志文件可以写;
  • 辅助程序可以执行,并继续继承当前 Profile。

wa 需要谨慎区分。若服务只需要追加日志,可以优先考虑:

/var/log/example/access.log a,

这样可以避免允许任意位置的覆盖写入。不过具体程序是否以追加方式打开文件,还取决于它使用的系统调用和 AppArmor 规则匹配结果。

2. 路径通配符

AppArmor 路径规则中的通配符不是普通 shell 通配符的简单复制。

常见形式:

/var/lib/example/* rw,
/var/lib/example/** rw,
/var/lib/example/*.db rw,

通常可以这样理解:

  • *:匹配路径字符,但不跨目录分隔符 /
  • **:可以跨越多个目录层级;
  • *.db:匹配当前目录下的 .db 文件;
  • **/*.db:匹配更深层目录中的 .db 文件。

例如:

/var/lib/example/* rw,

通常匹配:

/var/lib/example/a
/var/lib/example/data.db

但不匹配:

/var/lib/example/cache/data.db

而:

/var/lib/example/** rw,

会覆盖更深层路径。它很方便,但授权范围也更大,不能因为“服务需要状态目录”就无条件使用。

3. 文件权限不是目录遍历权限的完整替代

服务要读取:

/var/lib/example/data/state.db

通常不仅需要文件本身的读取规则,还需要能够遍历父目录。AppArmor 规则与 DAC 的目录执行权限共同影响路径解析。

如果日志中显示访问某个深层文件被拒绝,不能只盯着最后一个文件名,还要检查:

  • 父目录是否允许遍历;
  • DAC 是否允许目录执行;
  • 实际路径是否经过符号链接;
  • 服务是否使用了不同的工作目录或运行时目录;
  • 是否存在 chroot、容器或 mount namespace。

不要简单地把整个 / 加上 r 来“解决”问题,这会显著扩大读取范围。

4. 符号链接、硬链接和重命名边界

AppArmor 是以路径为中心的访问控制机制,但路径访问还受到内核文件对象语义影响。以下情况需要特别测试:

  • 符号链接最终指向的目标是否也被规则覆盖;
  • 服务是否通过临时文件加 rename() 完成原子更新;
  • 服务是否需要创建硬链接;
  • 服务是否使用 /proc/sys 或设备文件;
  • 服务是否会在启动后修改工作目录。

例如程序更新配置时可能不是直接写:

/etc/example/config.yaml

而是:

/etc/example/.config.yaml.tmp
rename(".config.yaml.tmp", "config.yaml")

此时 Profile 可能需要允许临时文件的创建、写入和重命名。只允许目标文件 rw 不一定足够。

5. 目录规则和文件规则不要混淆

一个实际配置应尽量按对象类型表达意图,而不是粗放地授权:

/var/lib/example/ r,
/var/lib/example/** rw,

第一条并不等价于“允许写目录”。目录创建、删除、重命名等行为有其独立的权限语义,具体需求应结合日志和程序行为验证。

如果程序只需要读取静态模板:

/usr/share/example/templates/** r,

就不要把整个 /usr/share/ 放开。


七、Include、变量和 Profile 组织方式

1. Include

发行版通常提供可复用的抽象规则,例如:

#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/ssl_certs>

这些抽象可能包含:

  • 基本动态链接库和运行时文件;
  • DNS、用户和组解析所需的路径;
  • TLS 证书;
  • 日志、时区和其他常见系统资源。

使用抽象的好处是减少重复规则,但必须查看其实际内容:

ls /etc/apparmor.d/abstractions/
sed -n '1,200p' /etc/apparmor.d/abstractions/base

抽象不是标准化的跨发行版 API。不同发行版和版本可能包含不同路径,Profile 迁移前需要重新验证。

2. 变量

常见变量定义位于 tunables 文件中:

@{HOME}=/home/*
@{PROC}=/proc/

Profile 可以写成:

@{HOME}/.config/example/** r,

变量用于减少重复并适应不同系统布局,但不要把变量当成运行时字符串拼接。它们属于 AppArmor Profile 解析阶段的宏或集合语义。

生产环境中应明确检查变量展开结果,因为:

@{HOME}/.config/example/** r,

可能覆盖多个用户目录,而不只是某个服务账号的目录。

3. Profile 与子 Profile

可以为执行的子程序定义专门 Profile:

/usr/sbin/example-service {
    /usr/bin/parser px -> example-parser,

    profile example-parser {
        /usr/bin/parser rix,
        /etc/example/parser.conf r,
        /var/lib/example/input/** r,
        /var/lib/example/output/** rw,
    }
}

这种结构的安全意义是:主服务和解析器不必共享全部权限。

一个合理的边界是:

主服务:监听端口、读取主配置、写状态
解析器:只读输入、写临时输出

如果解析器处理不可信文件,单独 Profile 可以降低解析器漏洞被利用后的影响范围。


八、能力、网络、信号和其他规则

文件规则只是 AppArmor 的一部分。现代服务还需要控制非文件操作。

1. Capability 规则

Linux capability 将部分 root 权限拆分为独立能力。AppArmor 可以限制进程是否使用某项能力:

capability net_bind_service,

这通常用于允许绑定低于 1024 的端口,而不允许服务获得全部 root 权限。

常见能力包括:

  • net_bind_service:绑定特权端口;
  • setuidsetgid:改变用户或组身份;
  • chown:改变文件所有者;
  • dac_override:绕过部分 DAC 检查;
  • sys_admin:范围很大的系统管理能力;
  • sys_time:修改系统时间。

原则是按日志和程序需求逐项增加,而不是加入:

capability,

后者会放开全部 capability,通常违背最小权限目标。

注意,AppArmor 的 capability 规则不等于 systemd 的 CapabilityBoundingSet=。二者可以叠加:

最终能力
=
程序需要的能力
∩
systemd 保留的能力
∩
AppArmor 允许的能力

2. Network 规则

AppArmor 可以控制网络相关的粗粒度属性,例如:

network inet stream,
network inet6 stream,
network unix stream,

它通常表达网络族、类型和协议层面的许可,例如 IPv4 TCP 流、IPv6 TCP 流或 Unix stream socket。

但在常见配置中,AppArmor 网络规则不等同于防火墙规则,通常不能替代:

  • nftables/iptables 的端口过滤;
  • IP 地址白名单;
  • HTTP 层访问控制;
  • TLS 客户端证书校验;
  • 服务自身的认证和授权。

因此以下目标应由不同机制负责:

AppArmor:进程是否可以使用某类 socket
nftables:数据包是否允许到达某地址和端口
应用配置:用户是否有权访问某个 API

3. 信号和进程交互

服务之间可能需要发送信号:

signal (send) set=(term, hup) peer=example-worker,

或需要调试、检查其他进程:

ptrace (trace) peer=example-debugger,

这类规则必须非常谨慎。ptrace 允许观察或影响其他进程,常常具有较高风险。生产服务不应为了让监控工具“方便采集”就无条件放开。

4. Mount、DBus、Unix 等规则

现代服务可能使用:

  • mount/umount;
  • DBus;
  • Unix domain socket;
  • 信号;
  • ptrace;
  • /proc/sys
  • 设备文件。

这类规则的语法和可用性会受到内核、AppArmor 工具版本及发行版策略影响。遇到需求时应以本机 apparmor_parser、发行版手册和内核文档为准,而不是直接复制其他系统的 Profile。

Linux 内核安全文档提供了 LSM 和 AppArmor 相关背景,可参考 Linux Security Documentation


九、一个可分析的 Profile 示例

下面给出一个服务 Profile 的骨架。假设程序具有以下行为:

可执行文件:/usr/local/sbin/example-service
配置文件:  /etc/example-service/config.yaml
状态目录:  /var/lib/example-service/
日志目录:  /var/log/example-service/
辅助程序:  /usr/bin/helper

Profile:

#include <tunables/global>

/usr/local/sbin/example-service {
    #include <abstractions/base>
    #include <abstractions/nameservice>

    /usr/local/sbin/example-service rix,

    /etc/example-service/ r,
    /etc/example-service/config.yaml r,

    /var/lib/example-service/ rw,
    /var/lib/example-service/** rw,

    /var/log/example-service/ rw,
    /var/log/example-service/*.log a,

    /usr/bin/helper ix,

    capability net_bind_service,
    network inet stream,
    network inet6 stream,
}

逐项解释:

  1. abstractions/base 提供常见运行时依赖,例如动态链接相关文件;
  2. abstractions/nameservice 允许常见名称解析路径,实际内容必须检查;
  3. 服务自身使用 rix,允许它作为可执行文件运行并继承当前 Profile;
  4. 配置目录只允许遍历,具体配置只允许读取;
  5. 状态目录允许读写;
  6. 日志文件使用追加权限,表达“写日志但不需要覆盖已有内容”的意图;
  7. 辅助程序使用 ix,执行后继续受到服务 Profile 约束;
  8. 只允许绑定特权端口,不放开全部 capability;
  9. 只允许使用声明的网络类型。

这个示例仍然可能不完整。例如服务可能需要:

  • 读取 /etc/resolv.conf
  • 读取 TLS 证书;
  • 创建 Unix socket;
  • 使用时区文件;
  • 写入 systemd 的运行时目录;
  • 访问 /run/example-service/
  • 执行 shell 脚本或外部压缩程序。

正确做法是通过测试和日志补齐实际需求,而不是预先把整个文件系统放开。


十、Profile 的加载、查看和状态确认

1. 查看 AppArmor 是否启用

sudo aa-status

也可以查看内核 LSM:

cat /sys/kernel/security/lsm

输出是否包含 apparmor 取决于内核启动参数和发行版配置。某些系统还会同时启用 SELinux、Smack 或其他 LSM,多个安全机制可能同时影响最终结果。

检查服务进程:

ps -eZ

ps -eZ 在不同 LSM 下的显示含义不同,不能仅凭这一个命令判断 AppArmor 是否生效。应结合:

sudo aa-status
sudo aa-status --profiled

以及日志确认目标进程实际绑定的 Profile。

2. 加载和重新加载 Profile

Profile 文件通常位于:

/etc/apparmor.d/

加载或重新加载某个 Profile:

sudo apparmor_parser -r /etc/apparmor.d/usr.local.sbin.example-service

-r 的常见含义是替换或重新加载现有 Profile。具体参数行为应以本机 man apparmor_parser 为准。

发行版也可能通过服务统一管理:

sudo systemctl reload apparmor

修改后应检查:

sudo aa-status
sudo journalctl -k -b | grep -i apparmor

不要把“命令没有报错”当成“服务已经按预期被约束”。还需要确认:

  • Profile 是否已加载;
  • 进程是否匹配该 Profile;
  • 服务是否重启或重新执行;
  • 新生成的子进程是否发生了预期转换;
  • 业务请求是否覆盖了关键代码路径。

3. 删除或禁用 Profile 的风险

禁用 Profile:

sudo aa-disable /etc/apparmor.d/usr.local.sbin.example-service

删除或禁用前应记录当前状态:

sudo aa-status > /root/apparmor-status.before

生产环境中,禁用 Profile 可能立即扩大服务的可访问范围。更安全的恢复策略通常是:

  1. 保留原 Profile;
  2. 先切换到 Complain 模式;
  3. 采集日志并确认影响;
  4. 修正规则;
  5. 重新加载;
  6. 再切回 Enforce。

十一、日志:从拒绝事件反推缺失规则

1. 典型拒绝日志

AppArmor 拒绝事件通常类似:

apparmor="DENIED" operation="open"
profile="/usr/local/sbin/example-service"
name="/etc/example-service/extra.conf"
pid=1234
comm="example-service"
requested_mask="r"
denied_mask="r"
fsuid=1001
ouid=0

字段的含义:

  • apparmor="DENIED":AppArmor 拒绝了操作;
  • operation="open":发生的内核操作类型;
  • profile=...:当时生效的 Profile;
  • name=...:被访问的路径或对象;
  • pid:触发事件的进程;
  • comm:进程名;
  • requested_mask:程序请求的权限;
  • denied_mask:最终被拒绝的权限;
  • fsuidouid:相关文件系统身份信息。

不同内核和工具版本可能显示不同字段。日志中的 name 也不一定直接等于应用配置里出现的字符串,因为程序可能经过符号链接、相对路径、临时文件或库调用。

2. 查看日志

优先从内核日志和 journald 中查询:

sudo journalctl -k -b | grep -i apparmor

查询最近的拒绝事件:

sudo journalctl -k --since "10 minutes ago" | grep 'apparmor="DENIED"'

如果系统运行 auditd,也可以:

sudo ausearch -m APPARMOR_DENIED -ts recent

事件类型名称和 auditd 是否安装取决于发行版。若命令没有结果,不代表一定没有拒绝,可能是:

  • AppArmor 没有加载;
  • 当前 Profile 处于允许模式;
  • 日志进入了其他日志通道;
  • auditd 未运行;
  • 查询过滤条件不匹配;
  • 服务尚未触发对应代码路径。

3. 从一条日志推导规则

假设看到:

apparmor="DENIED" operation="open"
profile="/usr/local/sbin/example-service"
name="/var/lib/example-service/cache/index.db"
requested_mask="rw"
denied_mask="rw"

推导步骤:

  1. 当前进程确实使用了 example-service Profile;

  2. 操作是打开文件,不是执行程序或网络连接;

  3. 程序请求读写权限;

  4. 目标路径位于状态目录下;

  5. 如果业务设计确认该数据库需要读写,可以增加:

    /var/lib/example-service/cache/index.db rw,
    
  6. 如果该目录中所有状态文件都属于服务,并且文件名动态生成,可以考虑:

    /var/lib/example-service/** rw,
    
  7. 但第二种规则授权范围更大,需要确认服务是否可能被利用后写入该目录中的敏感文件。

反例是直接添加:

/** rw,

这会让 Profile 几乎失去文件访问约束,不能视为合理修复。

4. requested_mask 不是自动生成规则

日志告诉你程序请求了什么,不代表程序应该被允许什么。

例如:

requested_mask="w"
name="/etc/passwd"

可能是:

  • 程序确实需要修改用户数据库;
  • 程序配置错误;
  • 被攻击者诱导执行了恶意路径;
  • 该服务本来就不应该拥有写入权限。

因此修复流程必须先回答业务问题:

这个操作是服务正常功能的一部分吗?

只有答案为“是”,才考虑补充规则。


十二、从 Complain 到 Enforce 的完整流程

第一步:确认已有 Profile

sudo aa-status
sudo ls -l /etc/apparmor.d/

如果发行版已经为目标程序提供 Profile,优先修改或扩展已有策略,避免同时创建两个互相冲突的 Profile。

第二步:切换到 Complain

sudo aa-complain /etc/apparmor.d/usr.local.sbin.example-service
sudo systemctl restart example-service

重启是为了让主进程和它的启动路径重新经过 Profile 匹配。对于已经运行的进程,具体行为取决于 Profile 是否被重新加载以及进程当前状态,不能只依赖“修改文件后自动生效”。

第三步:覆盖正常业务路径

测试应至少包括:

  • 服务冷启动;
  • 正常请求;
  • 配置重载;
  • 日志轮转;
  • 状态持久化;
  • TLS 证书读取;
  • DNS 解析;
  • 外部辅助程序调用;
  • 定时任务;
  • 异常重启;
  • 升级和回滚路径。

只测试一次首页请求,无法证明 Profile 覆盖了真实工作集。

第四步:分析日志并分类

把事件分为:

应当允许的正常行为
不应出现的异常行为
由测试环境特有的行为
暂时无法判定的行为

只为第一类补规则。

第五步:重新加载和验证

sudo apparmor_parser -r /etc/apparmor.d/usr.local.sbin.example-service
sudo aa-enforce /etc/apparmor.d/usr.local.sbin.example-service
sudo systemctl restart example-service
sudo systemctl status example-service

然后重新执行业务测试,并确认日志中没有新的预期拒绝:

sudo journalctl -k --since "5 minutes ago" | grep 'profile="/usr/local/sbin/example-service"'

第六步:保留可恢复版本

在生产发布前保留:

  • Profile 文件版本;
  • 加载前后的 aa-status
  • 服务启动日志;
  • 业务回归结果;
  • 回滚命令;
  • 变更对应的包版本和内核版本。

AppArmor 规则错误的主要故障表现不是“系统无法启动”,而是某个服务在特定请求或异常路径下失败,因此回归和回滚都必须可执行。


十三、服务加固:AppArmor 与 systemd 如何配合

AppArmor 负责进程的 MAC 约束,systemd 负责服务生命周期和一部分沙箱边界。二者不是互相替代关系。

1. 使用 systemd 指定 AppArmor Profile

某些 systemd 版本支持在服务单元中指定:

[Service]
AppArmorProfile=usr.local.sbin.example-service

示例 Drop-in:

sudo systemctl edit example-service

写入:

[Service]
AppArmorProfile=usr.local.sbin.example-service
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/example-service /var/log/example-service

然后:

sudo systemctl daemon-reload
sudo systemctl restart example-service
sudo systemctl status example-service

这些选项的作用不同:

  • AppArmorProfile=:指定 AppArmor Profile;
  • NoNewPrivileges=yes:阻止进程通过 exec 获得新的特权;
  • PrivateTmp=yes:为服务提供独立临时目录视图;
  • ProtectSystem=strict:把系统目录以只读方式暴露给服务;
  • ProtectHome=yes:隐藏或限制用户主目录;
  • ReadWritePaths=:在 systemd 文件系统沙箱中声明可写目录。

需要注意,ReadWritePaths= 不能自动绕过 AppArmor;systemd 允许写入的目录还必须同时满足 AppArmor 和 DAC。

配置前检查当前系统是否支持这些指令:

systemd-analyze verify /etc/systemd/system/example-service.service

发行版可能使用不同的 unit 文件位置和 systemd 版本。若 AppArmorProfile= 不被支持,应通过程序执行路径和 AppArmor 的正常附着机制绑定 Profile。

2. systemd 与 AppArmor 的故障路径

假设服务启动失败,可能有三条不同路径:

systemd 沙箱拒绝
    -> 服务收到 Permission denied
    -> AppArmor 日志可能没有对应 DENIED

AppArmor 拒绝
    -> 服务收到 Permission denied 或 exec 失败
    -> 内核日志出现 apparmor="DENIED"

DAC 拒绝
    -> 服务收到 Permission denied
    -> 只有在其他审计机制记录时才可能看到 DAC 线索

因此不能看到 Permission denied 就直接修改 AppArmor。应同时检查:

systemctl status example-service
journalctl -u example-service -b
journalctl -k -b | grep -i apparmor
namei -l /path/to/file

namei -l 可以逐级查看路径上的目录权限,有助于区分 DAC 和 AppArmor 问题。

3. 服务运行用户与 Profile 的关系

AppArmor Profile 绑定的是进程,而不是简单绑定 UID:

同一个用户 UID
    ├── 程序 A:可能绑定 profile-a
    └── 程序 B:可能绑定 profile-b 或 unconfined

因此把服务改成非 root 用户仍然有价值,但不等于可以省略 AppArmor;反过来,配置了 AppArmor 也不等于可以让服务继续以 root 运行。

合理的分层通常是:

systemd User=example
    +
文件 owner/group/ACL
    +
AppArmor Profile
    +
NoNewPrivileges、CapabilityBoundingSet 等
    +
nftables、seccomp 或容器边界

每层解决不同问题,最终权限是多个边界的交集。


十四、以 OpenSSH 为例理解服务加固边界

OpenSSH 的权限控制至少有三层:

  1. sshd_config 控制 SSH 协议和认证行为;
  2. Linux DAC 控制用户对密钥、主目录和系统文件的访问;
  3. AppArmor 可以限制 sshd 及其子进程的额外文件、执行和能力行为。

例如 SSH 配置可以控制:

PermitRootLogin no
PasswordAuthentication no
AllowUsers deploy

但这些配置不能表达:

sshd 只能读取某些主机密钥文件;
登录后的会话不能访问某个服务密钥;
sshd 不能执行额外的诊断程序。

这类进程行为边界可以由 AppArmor 或其他沙箱机制补充。

不过 OpenSSH 涉及:

  • PAM;
  • 用户认证;
  • 主机密钥;
  • 用户主目录;
  • authorized_keys
  • shell;
  • scpsftp
  • ChrootDirectory
  • systemd socket activation 或发行版启动脚本。

因此不应直接把网上某个 sshd Profile 复制到生产环境。应先确认本机已有策略:

sudo aa-status
sudo find /etc/apparmor.d -maxdepth 1 -type f | grep -E 'ssh|sshd'

再用 OpenSSH 官方手册核对服务行为和配置含义:OpenSSH Manual Pages

如果目标是限制某一类 SSH 用户,OpenSSH 自身的 Match UserForceCommandChrootDirectory 往往比给整个 sshd 进程添加粗粒度 AppArmor 规则更直接。AppArmor 更适合约束 sshd 进程及其执行链路的系统访问,但不能替代 SSH 协议配置中的认证和授权逻辑。


十五、常见失败表现与诊断方法

1. 服务启动即失败

先看服务日志:

sudo systemctl status example-service
sudo journalctl -u example-service -b

再看内核日志:

sudo journalctl -k -b | grep -i apparmor

如果看到:

apparmor="DENIED" operation="exec"

说明服务执行了 Profile 不允许的程序。需要判断:

  • 该程序是否是正常依赖;
  • 是否应该 ix 继承限制;
  • 是否应当 px 切换到更窄的 Profile;
  • 是否根本不应该执行它。

不要看到执行失败就直接写:

/** ix,

这会让服务几乎可以执行任意路径并继承当前 Profile,严重削弱执行边界。

2. 配置读取失败

典型日志:

apparmor="DENIED" operation="open"
name="/etc/example-service/production.yaml"
requested_mask="r"

检查:

namei -l /etc/example-service/production.yaml
sudo ls -lZ /etc/example-service/production.yaml

其中 ls -lZ 在启用 SELinux 等机制的系统上可能提供额外安全上下文信息;它不是 AppArmor 状态的主要判断工具,但有助于识别多个 LSM 同时生效的情况。

确认 DAC 允许服务用户读取后,再增加最窄规则:

/etc/example-service/production.yaml r,

不要因为配置文件读取失败就授权整个 /etc

3. 日志正常但业务仍失败

可能原因包括:

  • Profile 没有匹配目标进程;
  • 日志查询范围错误;
  • 服务运行在另一个 mount namespace;
  • 失败来自 DAC、systemd、seccomp 或应用自身;
  • AppArmor 处于 Complain 模式,操作被记录但没有被阻止;
  • 子进程发生了 Profile 转换,拒绝日志中的 Profile 名称不是主服务名。

应核对实际进程树:

pstree -ap "$(pidof example-service | awk '{print $1}')"
sudo aa-status

并分别检查服务管理器、内核和应用日志。

4. 加载成功但规则似乎没生效

可能是 Profile 名称、附着路径或执行方式不一致。例如:

  • 实际程序是 /usr/libexec/example-service,而规则写成 /usr/sbin/example-service
  • systemd 启动的是符号链接或包装脚本;
  • 程序启动后执行了真正的二进制文件;
  • 容器运行时使用了不同的路径视图;
  • Profile 文件被修改但没有重新加载。

诊断实际可执行文件:

readlink -f /proc/"$(pidof example-service | awk '{print $1}')"/exe

查看进程状态:

sudo cat /proc/"$(pidof example-service | awk '{print $1}')"/attr/current

如果该接口存在,它通常可以帮助确认当前进程的 LSM 安全状态;不同内核和 LSM 组合下输出格式可能不同。


十六、容器、路径命名空间和 AppArmor

AppArmor 主要由宿主机内核实施。容器内的路径、执行文件和 mount namespace 可能与宿主机看到的路径不同,但内核仍然负责执行最终检查。

这带来几个工程边界:

  1. Profile 通常需要在宿主机加载;
  2. 容器运行时可能要求显式指定 AppArmor Profile;
  3. 容器中的 /etc 不一定对应宿主机的 /etc
  4. 镜像升级可能改变路径和执行链;
  5. 一个 Profile 不一定适合所有镜像版本;
  6. Kubernetes、Docker、Podman 的指定方式不同。

因此容器场景不能只把宿主机服务 Profile 原样复制进去。应同时确认:

  • 运行时是否真正为容器进程设置了 Profile;
  • 容器内的主进程和子进程是否经过预期转换;
  • mount namespace 是否改变了路径匹配;
  • 容器是否额外使用 seccomp、capability drop 和 read-only root filesystem。

AppArmor 是容器安全的一层,而不是完整的容器隔离模型。


十七、AppArmor 与 SELinux 的主要区别

AppArmor 和 SELinux 都是 MAC 机制,但策略模型不同。

AppArmor

典型特点:

  • 以 Profile 为主要组织单位;
  • 常以路径规则描述文件访问;
  • 对已有服务建立策略相对直观;
  • 日志通常容易映射回路径;
  • 规则与程序执行路径联系紧密。

SELinux

典型特点:

  • 以安全上下文、Label、Type Enforcement 为核心;
  • 访问控制通常依赖对象类型和进程类型;
  • 对文件重命名、路径别名和系统对象关系的表达方式不同;
  • 策略模型更系统化,但学习和维护成本也较高。

二者不是简单的“谁更安全”。安全效果取决于:

策略是否覆盖真实行为
+
是否处于强制模式
+
是否持续维护
+
日志是否被监控
+
服务本身是否采用最小权限

启用多个 LSM 时,访问通常需要同时通过多个检查。排障时要确认到底是哪一层拒绝,而不是将所有 Permission denied 都归因于 AppArmor。


十八、生产环境中的取舍

1. 越窄的规则不一定越稳定

最小权限要求授权越窄越好,但服务升级、插件启用、日志轮转和故障恢复都可能引入新访问路径。

例如:

/var/lib/example/** rw,

稳定性较高,但范围较宽;

/var/lib/example/index.db rw,
/var/lib/example/cache/* rw,

范围较窄,但程序新增状态文件时可能失败。

实际策略应基于服务的稳定工作集,并通过版本化测试维护,而不是追求一份永不变化的 Profile。

2. 不要把日志当成唯一证据

日志能证明“某个路径在某次运行中被访问过”,不能证明:

  • 所有正常路径都已覆盖;
  • 没有敏感数据暴露;
  • 该操作从安全角度应该被允许;
  • 未来版本不会改变执行行为。

日志是策略设计的证据之一,业务模型、程序文档、源代码或系统调用追踪也可能需要参与判断。

3. 不要盲目使用自动生成规则

aa-logprof 等工具可以根据日志提出规则建议:

sudo aa-logprof

它适合减少手工整理日志的工作量,但自动建议不等于安全结论。每一条建议都应检查:

  • 路径是否过于宽泛;
  • 是否来自正常业务;
  • 是否应该允许写而不是只读;
  • 是否应使用 ix 还是 Profile 转换;
  • 是否会为攻击者提供读取密钥、执行 shell 或写入敏感目录的能力。

尤其要警惕自动生成的宽泛通配符和执行权限。


十九、部署前检查清单

可以用以下顺序进行一次实际 Profile 评审:

1. 目标进程是谁,实际可执行路径是什么?
2. 服务以哪个 UID/GID 运行?
3. 进程当前是否真的绑定了目标 Profile?
4. 配置、证书、状态、日志和运行时目录分别是什么?
5. 哪些文件只读,哪些文件需要追加,哪些文件需要读写?
6. 哪些子程序会被执行,执行后应继承还是切换 Profile?
7. 是否需要 capability、网络、Unix socket、信号或 ptrace?
8. systemd 是否还有额外的文件系统或能力限制?
9. 正常启动、重载、轮转、升级和恢复流程是否都测试过?
10. Enforce 模式下是否检查过服务日志和内核审计日志?
11. 失败时是否有明确的降级或回滚方案?
12. Profile 是否纳入版本控制并与服务版本一起发布?

其中第 3 项最容易被忽略:Profile 文件写得再好,如果没有加载、没有匹配进程或进程已经转入另一个 Profile,实际保护效果都不是预期结果。


二十、结语:把 Profile 当作可验证的进程权限模型

理解 AppArmor 的关键,不是记住若干条规则,而是建立下面这条因果链:

程序启动
    -> 匹配或继承 Profile
    -> 执行路径决定是否发生 Profile 转换
    -> 文件、能力、网络和进程操作触发内核检查
    -> Mode 决定拒绝、记录或终止
    -> 审计日志暴露实际工作集
    -> 工作集与安全意图共同决定最终规则

Profile 描述的是进程权限边界,Mode 决定违规时的运行行为,规则表达具体操作,日志提供验证证据,systemd 和其他内核安全机制则补充生命周期、能力和资源隔离。

生产加固的目标不是让所有拒绝日志消失,而是让每一条被允许的行为都有明确业务理由,让每一条被拒绝的行为都能被定位、验证和恢复。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。