第一百六十六章:AppArmor
渡劫期涉及内核源码:
一
LSM 框架的大道之上,林小源还未走远,便听到身后传来一个轻快的声音。
"等等!你还没认识我呢。"
林小源回头,看到一个穿着素白长袍的年轻修士,面容清秀,腰间挂着一卷竹简。与 SELinux 那位浑身刻满标签的铁面判官不同,这人看起来随和得多。
"你是?"
"AppArmor,"那人拱手一笑,"路径型安全模块。你别看我简单,在 Ubuntu 那边,我才是默认的守护者。"
林小源好奇地打量他。AppArmor 的竹简上没有标签,只写着一行行清晰的路径——/var/www/html/** r,、/var/log/httpd/** w,。
"你的规则……直接写路径?"
"对,"AppArmor 展开竹简,"不像 SELinux 那样要给每个文件贴标签。我直接告诉进程:你能访问哪些路径,不能访问哪些。简单明了。"
他指着竹简上的条目:"你看,这个 httpd 的 profile——/var/www/html/** r 只读网页,/var/log/httpd/** wa 可以写日志和追加,/etc/httpd/conf/** r 只读配置。一目了然。"
林小源点点头,但随即皱眉:"可是……如果文件被移动了呢?比如有人把网页从 /var/www/html/ 移到了 /srv/www/?"
AppArmor 的笑容微微一僵。
"这……确实是个问题。我认的是路径,不是文件本身。路径一变,规则就失效了。"
"所以 SELinux 的标签方式更可靠?"
AppArmor 沉默片刻,然后坦然道:"在精确性上,是的。但简单也有简单的好处——上手快,维护容易。在容器和桌面场景下,我的路径规则够用了。你不需要为每个文件打标签,一条通配符就能覆盖整个目录。"
/*
* AppArmor vs SELinux:
*
* SELinux:
* 基于标签 (type)
* 需要给每个文件设置标签
* 复杂但精确
*
* AppArmor:
* 基于路径
* profile 定义可访问路径
* 简单但路径变化可能失效
*
* AppArmor profile 示例:
* /usr/sbin/httpd {
* /var/www/html/** r,
* /var/log/httpd/** w,
* /etc/httpd/conf/** r,
* network inet tcp,
* capability net_bind_service,
* }
*
* profile 模式:
* enforce — 强制执行
* complain — 只记录
*
* 工具:
* aa-enforce — 设置 enforce 模式
* aa-complain — 设置 complain 模式
* aa-genprof — 生成 profile
* aa-logprof — 从日志生成规则
*/
/* 模拟 AppArmor profile */
struct profile_rule {
char path[128];
char perms[32]; /* r, w, x, a (append) */
};
struct aa_profile {
char name[64];
char mode[16]; /* enforce, complain */
struct profile_rule rules[10];
int rule_count;
};
void add_rule(struct aa_profile *p, const char *path, const char *perms) {
if (p->rule_count >= 10) return;
strncpy(p->rules[p->rule_count].path, path, 127);
strncpy(p->rules[p->rule_count].perms, perms, 31);
p->rule_count++;
}
int check_access(struct aa_profile *p, const char *path, char perm) {
for (int i = 0; i < p->rule_count; i++) {
/* 简化的路径匹配 */
if (strstr(path, p->rules[i].path + 1) != NULL ||
strstr(p->rules[i].path, path) != NULL) {
if (strchr(p->rules[i].perms, perm))
return 1; /* 允许 */
}
}
return 0; /* 拒绝 */
}
void print_profile(struct aa_profile *p) {
printf("Profile: %s (%s)\n", p->name, p->mode);
for (int i = 0; i < p->rule_count; i++) {
printf(" %s %s\n", p->rules[i].path, p->rules[i].perms);
}
}
printf("=== AppArmor — 路径型安全 ===\n\n");
/* 创建 httpd profile */
struct aa_profile httpd = {
.name = "/usr/sbin/httpd",
.mode = "enforce",
.rule_count = 0
};
add_rule(&httpd, "/var/www/html/", "r");
add_rule(&httpd, "/var/log/httpd/", "wa");
add_rule(&httpd, "/etc/httpd/conf/", "r");
add_rule(&httpd, "/tmp/", "rw");
printf("--- AppArmor Profile ---\n");
print_profile(&httpd);
printf("\n--- 访问检查 ---\n");
/* 测试不同的访问 */
struct {
const char *path;
char perm;
const char *desc;
} tests[] = {
{"/var/www/html/index.html", 'r', "读取网页"},
{"/var/www/html/index.html", 'w', "写入网页"},
{"/var/log/httpd/access.log", 'w', "写入日志"},
{"/etc/passwd", 'r', "读取密码文件"},
{"/tmp/sess_abc123", 'r', "读取 session"},
};
int n = sizeof(tests) / sizeof(tests[0]);
for (int i = 0; i < n; i++) {
int allowed = check_access(&httpd, tests[i].path, tests[i].perm);
printf("%s: %s\n", tests[i].desc, allowed ? "允许" : "拒绝");
}
printf("\n--- AppArmor vs SELinux ---\n");
printf("AppArmor:\n");
printf(" 基于路径\n");
printf(" 简单易用\n");
printf(" 路径变化可能失效\n\n");
printf("SELinux:\n");
printf(" 基于标签\n");
printf(" 复杂精确\n");
printf(" 不受路径变化影响\n\n");
printf("--- 使用场景 ---\n");
printf("AppArmor:\n");
printf(" Ubuntu 默认\n");
printf(" 容器安全\n");
printf(" 桌面应用\n\n");
printf("SELinux:\n");
printf(" RHEL/CentOS 默认\n");
printf(" 服务器\n");
printf(" 高安全需求\n");#include <stdio.h>
#include <string.h>
/*
* AppArmor vs SELinux:
*
* SELinux:
* 基于标签 (type)
* 需要给每个文件设置标签
* 复杂但精确
*
* AppArmor:
* 基于路径
* profile 定义可访问路径
* 简单但路径变化可能失效
*
* AppArmor profile 示例:
* /usr/sbin/httpd {
* /var/www/html/** r,
* /var/log/httpd/** w,
* /etc/httpd/conf/** r,
* network inet tcp,
* capability net_bind_service,
* }
*
* profile 模式:
* enforce — 强制执行
* complain — 只记录
*
* 工具:
* aa-enforce — 设置 enforce 模式
* aa-complain — 设置 complain 模式
* aa-genprof — 生成 profile
* aa-logprof — 从日志生成规则
*/
/* 模拟 AppArmor profile */
struct profile_rule {
char path[128];
char perms[32]; /* r, w, x, a (append) */
};
struct aa_profile {
char name[64];
char mode[16]; /* enforce, complain */
struct profile_rule rules[10];
int rule_count;
};
void add_rule(struct aa_profile *p, const char *path, const char *perms) {
if (p->rule_count >= 10) return;
strncpy(p->rules[p->rule_count].path, path, 127);
strncpy(p->rules[p->rule_count].perms, perms, 31);
p->rule_count++;
}
int check_access(struct aa_profile *p, const char *path, char perm) {
for (int i = 0; i < p->rule_count; i++) {
/* 简化的路径匹配 */
if (strstr(path, p->rules[i].path + 1) != NULL ||
strstr(p->rules[i].path, path) != NULL) {
if (strchr(p->rules[i].perms, perm))
return 1; /* 允许 */
}
}
return 0; /* 拒绝 */
}
void print_profile(struct aa_profile *p) {
printf("Profile: %s (%s)\n", p->name, p->mode);
for (int i = 0; i < p->rule_count; i++) {
printf(" %s %s\n", p->rules[i].path, p->rules[i].perms);
}
}
int main() {
printf("=== AppArmor — 路径型安全 ===\n\n");
/* 创建 httpd profile */
struct aa_profile httpd = {
.name = "/usr/sbin/httpd",
.mode = "enforce",
.rule_count = 0
};
add_rule(&httpd, "/var/www/html/", "r");
add_rule(&httpd, "/var/log/httpd/", "wa");
add_rule(&httpd, "/etc/httpd/conf/", "r");
add_rule(&httpd, "/tmp/", "rw");
printf("--- AppArmor Profile ---\n");
print_profile(&httpd);
printf("\n--- 访问检查 ---\n");
/* 测试不同的访问 */
struct {
const char *path;
char perm;
const char *desc;
} tests[] = {
{"/var/www/html/index.html", 'r', "读取网页"},
{"/var/www/html/index.html", 'w', "写入网页"},
{"/var/log/httpd/access.log", 'w', "写入日志"},
{"/etc/passwd", 'r', "读取密码文件"},
{"/tmp/sess_abc123", 'r', "读取 session"},
};
int n = sizeof(tests) / sizeof(tests[0]);
for (int i = 0; i < n; i++) {
int allowed = check_access(&httpd, tests[i].path, tests[i].perm);
printf("%s: %s\n", tests[i].desc, allowed ? "允许" : "拒绝");
}
printf("\n--- AppArmor vs SELinux ---\n");
printf("AppArmor:\n");
printf(" 基于路径\n");
printf(" 简单易用\n");
printf(" 路径变化可能失效\n\n");
printf("SELinux:\n");
printf(" 基于标签\n");
printf(" 复杂精确\n");
printf(" 不受路径变化影响\n\n");
printf("--- 使用场景 ---\n");
printf("AppArmor:\n");
printf(" Ubuntu 默认\n");
printf(" 容器安全\n");
printf(" 桌面应用\n\n");
printf("SELinux:\n");
printf(" RHEL/CentOS 默认\n");
printf(" 服务器\n");
printf(" 高安全需求\n");
return 0;
}二
AppArmor 带着林小源来到一处容器营地前。营地四周立着半透明的光幕,上面浮动着路径规则。
"你看,"AppArmor 指向营地内一个正在运行的 httpd 进程,"我给它加载了 enforce 模式的 profile。它只能读网页目录、写日志目录、读配置目录。别的路径,一概碰不了。"
林小源注意到营地角落里有一个测试进程试图访问 /etc/passwd,光幕立刻闪烁红光,将其挡了回来。
"但如果有人在 complain 模式下呢?"
AppArmor 摸了摸下巴:"complain 模式只记录不阻止。开发的时候用它,看看进程需要哪些权限,再据此写 profile。写好了,切成 enforce。"
"像是一种……试探?"
"对,先观察,再收紧。比 SELinux 那种上来就铁面无私要温和得多。"AppArmor 顿了顿,"不过也正因如此,有些管理员嫌麻烦,一直留在 complain 模式——那等于没有防护。"
林小源皱起眉头。光幕上那些路径规则清晰易懂,但正因为简单,反而容易被人忽视。
三
营地深处,林小源看到一台巨大的容器编排器正在为新容器加载 profile。
"每次容器启动,"AppArmor 跟上来,"运行时都会根据镜像的配置,把对应的 profile 注入内核。容器里的进程一睁眼,就已经被我的规则包围了。"
编排器发出低沉的嗡鸣,一条条路径规则从竹简上剥离,化作光纹印在容器的防护罩上。
"即使容器逃逸呢?"林小源问。
AppArmor 的眼神变得认真起来:"逃逸出去的进程仍然受 profile 约束。它能访问的路径不会因为逃逸而扩大。这就是纵深防御——容器隔离是一层,我的 profile 又是一层。"
林小源沉默片刻。他想起了 SELinux 的标签、capabilities 的权限位、seccomp 的系统调用白名单——每一层都不完美,但叠在一起,就成了一道难以逾越的墙。
"简单,"他低声说,"有时候也是一种力量。"
AppArmor 笑了笑,重新卷起竹简:"走吧,前面还有更深层的防御在等你。"
道藏笔记
内核启示
AppArmor 和 SELinux 走的是两条路。SELinux 给每个文件贴标签,AppArmor 直接写路径——/var/www/html/** r、/var/log/httpd/** wa,一目了然。简单是简单,但文件被移了路径规则就失效,这是它的软肋。
profile 有两种模式:enforce 真正拦截,complain 只记不拦(开发时用,先观察进程需要什么权限再写规则)。Ubuntu 默认用 AppArmor,容器安全和桌面场景也常见它的身影。上手快,维护容易,对很多场景来说够用了。
AppArmor 是路径守护——简单但实用。
AppArmor 之试
AppArmor 不是按 SELinux type,而是用哪种按程序路径绑定的配置约束权限?