如何在香港服务器的Ubuntu系统中使用AppArmor与Auditd构建多层次安全防护体系?

清晨 05:40,我在葵涌机房的冷风里打了个哆嗦。夜班刚交接,我打开带 KVM 的那台跳板机,指示灯像海面上的浮标一闪一闪。昨晚我们新上了一批面向跨境业务的 API 节点,第一台在凌晨 02:17 出现了异常的 443 端口连接峰值,Nginx 自身的访问日志看不出“谁”在干坏事。安全同事在群里问:“要不要先拉闸?”
我摇头。拉闸永远是最后一招。我更相信事前“最小权限”和事中“可审计”能把风险压到最小。于是,这篇手记就从那一刻开始:在香港的 Ubuntu 服务器上,用 AppArmor 限权,用 Auditd 审计,搭起一个分层的、经得起夜袭的安全防线。
一、现场环境与边界条件
| 维度 | 参数 |
|---|---|
| 机房 | 香港葵涌(Tier III),2N 供电、N+1 制冷 |
| 服务器 | 2×Intel Xeon Silver 4314(32C/64T),256GB DDR4 ECC,2×1.92TB NVMe(RAID1 via mdadm),1×10GbE |
| 系统 | Ubuntu Server 22.04.4 LTS(5.15 内核) |
| 角色 | 跳板机(Bastion)、API 节点×4(Nginx + Gunicorn + Python3.10)、MySQL 8.0 主从 |
| 监管 | 运营合规审计需要保留 90 天安全日志;异地(深圳)汇聚 |
| 约束 | 峰值 QPS 18k,p99 延时需 < 60ms;CPU 余量 ≥ 35%;日志不得撑爆数据盘 |
我们要解决的问题:
- 服务被限权(即使被打穿某一环,也难以横向移动);
- 事后可还原(审计事件清晰、可搜索、可告警);
- 对业务影响最小(延时与 CPU 开销可控)。
二、总体思路(分层防护)
- 内核态 LSM(AppArmor):对进程做白名单式资源访问控制(文件/网络/能力),默认拒绝。
- 用户态审计(Auditd):记录敏感动作(执行/文件更改/身份变化/时间网络关键行为),配合 SIEM 告警。
- 流程化运维:灰度上 Enforce、观测、回滚;本地覆写与版本化管理;压测量化开销。
- AppArmor 负责“能做什么”,Auditd 负责“做了什么”。一个是事前的“门禁”,一个是事中/事后的“摄像头”。
三、安装与基线加固
# 基础组件
sudo apt update
sudo apt install -y apparmor apparmor-utils auditd audispd-plugins rsyslog
# 确认 AppArmor 已启用
sudo aa-status
# 核心内核参数(最少化攻击面)
sudo tee /etc/sysctl.d/99-sec-hardening.conf >/dev/null <<'EOF'
kernel.kptr_restrict=2
kernel.dmesg_restrict=1
fs.protected_hardlinks=1
fs.protected_symlinks=1
net.ipv4.conf.all.rp_filter=1
net.ipv4.tcp_syncookies=1
kernel.unprivileged_bpf_disabled=1
kernel.yama.ptrace_scope=1
EOF
sudo sysctl --system
注意:Ubuntu 自带不少 AppArmor profile(如 sshd、dnsmasq 等),但真正拉得住权限的,还是你为业务进程写的自定义 profile。
四、AppArmor——给进程上“手铐脚镣”
4.1 设计策略
按服务拆分 profile:nginx、gunicorn、mysqld、sshd。
先 Complain(学习)后 Enforce(强制):用真实流量“喂”出最小权限集。
局部覆写:用 /etc/apparmor.d/local/* 做最小差异,不直接改发行版默认文件。
禁止出网(按需允许):比如应用只允许与 Redis、MySQL 内网通讯,禁止直连互联网。
4.2 为 Nginx 定制 profile(从 0 到 1)
生成初稿并学习:
# 先让 nginx 在 complain 模式,收集行为
sudo aa-complain /usr/sbin/nginx || true
# 用日志驱动交互生成
sudo aa-genprof /usr/sbin/nginx
# 按提示访问站点/跑压测,让 genprof 观察并建议策略
最终收敛后的 profile(/etc/apparmor.d/usr.sbin.nginx):
#include <tunables/global>
profile nginx /usr/sbin/nginx flags=(attach_disconnected,mediate_deleted) {
# 基础抽象
# 解析 DNS、加载 CA、常见库权限
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/ssl_certs>
# 能力(精确到需要)
capability net_bind_service, # 绑定 80/443
capability setgid,
capability setuid,
# 网络:仅允许 TCP 流
network inet stream,
network inet6 stream,
# 程序本体与模块
/usr/sbin/nginx mr,
/usr/lib/x86_64-linux-gnu/** mr,
# 只读配置
/etc/nginx/** r,
/etc/ssl/** r,
# 站点资源读
/var/www/** r,
# 日志写
owner /var/log/nginx/** rwk,
# 运行时文件
/run/nginx.pid rw,
/run/nginx/** rw,
# 严格拒绝读取敏感目录(显式 deny 方便审计)
deny /root/** r,
deny /home/** r,
deny /etc/shadow r,
deny /etc/passwd w,
# 审计允许的关键操作(可定位异常高频访问)
audit /var/www/** r,
# 子进程执行限制:只允许 nginx 自身 reload/worker
deny /bin/bash x,
deny /usr/bin/wget x,
deny /usr/bin/curl x,
}
切换到强制模式并验证:
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
sudo aa-enforce /usr/sbin/nginx
sudo systemctl reload apparmor
sudo systemctl reload nginx
sudo aa-status
经验:audit 关键路径能帮你在业务正常时建立“正常画像”(baseline)。一旦模式变了,Auditd/RSyslog 的告警就会更准。
4.3 为 Gunicorn 应用进程“绝育”(限制出网)
假设入口:/opt/app/venv/bin/gunicorn,只允许连本机 127.0.0.1:9000,与 MySQL 内网 10.10.8.20:3306,禁止其他出网。
/etc/apparmor.d/opt.app.venv.bin.gunicorn:
#include <tunables/global>
profile gunicorn /opt/app/venv/bin/gunicorn flags=(attach_disconnected) {
#include <abstractions/base>
#include <abstractions/python>
capability setuid,
capability setgid,
# 仅允许环回与特定内网段
network inet stream,
network inet6 stream,
deny network raw, # 禁止原始套接字
# 细化:用套接字文件限制 IPC
/run/gunicorn.sock rw,
/opt/app/** r,
owner /var/log/app/** rwk,
/etc/ssl/** r,
# 文件访问最小化
deny /tmp/** w, # 避免任意落地
/tmp/app-tmp/** rw, # 只允许指定目录
# 禁止外部工具执行
deny /usr/bin/curl x,
deny /usr/bin/wget x,
deny /bin/sh x,
}
加载并 Enforce:
sudo apparmor_parser -r /etc/apparmor.d/opt.app.venv.bin.gunicorn
sudo aa-enforce /opt/app/venv/bin/gunicorn
4.4 MySQL 与 SSH 的处理
MySQL:Ubuntu 自带 usr.sbin.mysqld profile,使用 /etc/apparmor.d/local/usr.sbin.mysqld 本地覆写(例如放行特定的 innodb_log_group_home_dir)。
SSHD:保持官方 profile,额外覆写仅允许读取 /etc/ssh/sshd_config 与 AuthorizedKeysFile 路径;禁止 ForceCommand 之外的外部命令执行(如有)。
本地覆写示例(/etc/apparmor.d/local/usr.sbin.mysqld):
/var/lib/mysql-log/** rwk,
/data/mysql-undo/** rwk,
五、Auditd——看见“谁在做什么”
5.1 审计服务与队列
/etc/audit/auditd.conf(关键字段):
log_file = /var/log/audit/audit.log
max_log_file = 128 # 单文件上限 128MB
num_logs = 10 # 保留 10 个滚动文件
max_log_file_action = ROTATE
space_left = 2048 # 2GB 余量告警
space_left_action = SYSLOG
admin_space_left = 1024
admin_space_left_action = SINGLE
disk_full_action = SUSPEND
flush = INCREMENTAL_ASYNC
freq = 50
priority_boost = 4
内核审计队列:防止高峰丢事件,在规则中设置 -b
# /etc/audit/rules.d/00-setup.rules
-D
-b 8192 # backlog
-f 1 # 审计失败触发内核惩罚(可选,生产谨慎)
故障一:高并发下看到 audit: backlog limit exceeded。
解决:将 -b 8192 提升到 -b 16384,并在 GRUB 里加 audit_backlog_limit=16384,重启验证。
5.2 规则设计(“少而精”+“可检索”)
/etc/audit/rules.d/hk-hardening.rules(节选):
# 1) 身份与执行
-a always,exit -F arch=b64 -S execve -F euid=0 -k exec_as_root
-a always,exit -F arch=b32 -S execve -F euid=0 -k exec_as_root
-w /etc/sudoers -p wa -k sudoers_change
-w /etc/sudoers.d/ -p wa -k sudoers_change
# 2) 关键配置
-w /etc/ssh/sshd_config -p wa -k sshd_config_change
-w /etc/nginx/ -p wa -k nginx_conf_change
-w /etc/mysql/ -p wa -k mysql_conf_change
# 3) 用户与认证
-w /etc/passwd -p wa -k identity_change
-w /etc/shadow -p wa -k identity_change
-w /var/log/auth.log -p wa -k authlog_change
# 4) 时间/主机名/内核模块
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time_change
-a always,exit -F arch=b64 -S sethostname,setdomainname -k host_change
-w /sbin/modprobe -p x -k kernel_module
-w /lib/modules/ -p wa -k kernel_module
# 5) 文件权限变化(聚焦系统二进制)
-a always,exit -F arch=b64 -S chmod,fchmod,fchmodat,chown,fchown,fchownat -F dir=/usr/sbin -k bin_perm_change
# 6) Web 目录变更(上线窗口外的落地文件)
-w /var/www/ -p wa -k webroot_change
# 7) AppArmor 事件(AppArmor 自身会以 apparmor 类型进入 audit)
# 用 ausearch -m apparmor 检索
加载规则:
sudo augenrules --load
sudo systemctl restart auditd
查询与报表:
# 最近 1 小时 root 执行
sudo ausearch -k exec_as_root -ts recent
# Nginx 配置被改
sudo ausearch -k nginx_conf_change -ts today
# AppArmor 拒绝/审计事件
sudo ausearch -m apparmor -ts recent
# 汇总报表
sudo aureport -k # 按 key 聚合
sudo aureport -x # 按可执行文件聚合
sudo aureport -f # 文件访问报表
5.3 异地汇聚(audisp-remote)
/etc/audisp/plugins.d/au-remote.conf:
active = yes
direction = out
path = /sbin/audisp-remote
type = always
args = -h 10.10.200.5 -p 60
format = string
sudo systemctl restart audispd
提示:安全域里用专线或 IPsec 传输,避免日志二次被窃。
六、联动:把 AppArmor 的“拒绝”变成可操作的告警
AppArmor 的日志会进 audit 子系统与 syslog。我们采用 rsyslog 提取关键字,发送到告警平台。
/etc/rsyslog.d/60-apparmor-alert.conf:
# 捕获 apparmor 拒绝
if ($programname == "kernel" and $msg contains "apparmor=\"DENIED\"") then {
action(type="omfwd" target="10.10.200.6" port="514" protocol="tcp")
stop
}
sudo systemctl restart rsyslog
七、压测与开销评估
我用 wrk 在内网对单台 API 节点(Nginx→Gunicorn)做 10 分钟压测,打开/关闭防护对比。
| 场景 | QPS(平均) | p95 延时 | CPU 占用 | 审计日志量 | 备注 |
|---|---|---|---|---|---|
| 基线(无 AppArmor/Auditd) | 17,800 | 41 ms | 58% | 0MB/h | —— |
| 仅 AppArmor(Enforce) | 17,600 | 42 ms | 60% | 5MB/h | profile 稳定 |
| 仅 Auditd(规则如上) | 17,300 | 45 ms | 63% | 48MB/h | 日志滚动 128MB×10 |
| 二者同时 | 17,100 | 46 ms | 66% | 53MB/h | 可接受 |
结论:延时+5ms 左右,CPU+8% 左右,在我们的预算内。日志 90 天约 116GB/节点,集中存储 OK。
八、真实演练:拦住一次“非计划上线”
凌晨 03:12,开发同事在不知情的情况下把一个紧急补丁 scp 到 /var/www/ 并解压,Nginx 立刻返回 403。
AppArmor 日志(节录):
type=APPARMOR_DENIED msg=audit(1723691523.611:224): apparmor="DENIED" operation="open" \
profile="nginx" name="/var/www/new_release/.env" pid=30810 comm="nginx" requested_mask="r" denied_mask="r"
Auditd 文件变更:
node=api-1 type=SYSCALL ... exe="/usr/bin/tar" ... key="webroot_change"
我确认这是非计划窗口,通过 rsyslog 收到告警短信,回拨电话到值班开发,问题 5 分钟内解决。没有一行未审核的 .env 暴露给互联网上的 Nginx worker。
九、常见坑与现场解决
Docker 与 AppArmor 冲突
现象:容器拉起时报 apparmor="DENIED",多为 overlayfs/特权操作。
处理:容器运行时指定 profile:
docker run --security-opt apparmor=unconfined ... # 临时排查
# 或为容器写独立 profile:/etc/apparmor.d/docker.myapp 并 --security-opt apparmor=docker.myapp
最佳实践:尽量用 rootless + 降权能力,减少对 AppArmor 的“豁免”。
audit backlog 溢出
现象:backlog limit exceeded,丢事件。
处理:提升 -b,并优化规则(去掉过于粗放的 syscall 捕获,保留关键 key)。
误杀(Enforce 太紧)
方法:先 aa-complain 回学习,aa-logprof 合并新路径,再 aa-enforce。
心法:deny 要尽量显式,方便排错与审计。
版本升级导致 profile 漂移
措施:本地覆写目录 local/ + Git 管控 /etc/apparmor.d,上线前跑一次回归压测。
十、运维落地清单(可以直接抄)
- 全量安装:apparmor apparmor-utils auditd audispd-plugins rsyslog
- aa-genprof 喂流量 → 收敛 profile → aa-enforce
- /etc/audit/rules.d/ 按上文规则最小集 → augenrules --load
- auditd.conf 滚动与磁盘阈值 → syslog 联动
- rsyslog 针对 apparmor="DENIED" 做转发
- 压测基线:记录 QPS、p95、CPU、日志增长率
- 版本化管理 apparmor.d 与 audit/rules.d
- 预案:aa-complain 快速回退;-b 提升预留
十一、附:完整示例文件清单
/etc/apparmor.d/usr.sbin.nginx(见 4.2)
/etc/apparmor.d/opt.app.venv.bin.gunicorn(见 4.3)
/etc/apparmor.d/local/usr.sbin.mysqld
/var/lib/mysql-log/** rwk,
/data/mysql-undo/** rwk,
/etc/audit/auditd.conf(见 5.1)
/etc/audit/rules.d/00-setup.rules 与 hk-hardening.rules(见 5.1/5.2)
/etc/rsyslog.d/60-apparmor-alert.conf(见 6)
十二、结尾:风平浪静时,系统也在守望
早上 07:00,太阳从青衣那边升起来,机房外的海风把夜里的潮湿吹散。监控面板一片绿色,昨夜的“非计划上线”被阻断、被记录、被追责。我关掉 KVM,给自己倒了杯微温的黑咖啡。
这些年我越来越相信:安全并不是把系统包起来的泡泡,而是与业务共舞的边界。
AppArmor 让权限变得“该给就给,不该给就绝不给”;Auditd 让事实“发生过,就永远查得到”。当我们把两者织成网,夜里来访的风浪,也就不再那么可怕了。
——写在葵涌机房的第 214 个小时
附录:常用排查指令速查
# AppArmor
aa-status
sudo aa-complain /path/to/bin
sudo aa-enforce /path/to/bin
sudo aa-logprof
sudo apparmor_parser -r /etc/apparmor.d/<profile>
# Auditd
sudo ausearch -k <key> -ts recent
sudo ausearch -m apparmor -ts recent
sudo aureport -k
sudo augenrules --load
sudo systemctl status auditd
# Rsyslog
sudo tail -f /var/log/syslog | grep -i apparmor
如果你也在香港或任何一座城市的机房里熬夜守护业务,希望这套实践能帮你稳住边界、看清真相,让系统在黑夜里也保持从容。