上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-19 09:54 阅读量:794


清晨 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,上线前跑一次回归压测。

十、运维落地清单(可以直接抄)

  1.  全量安装:apparmor apparmor-utils auditd audispd-plugins rsyslog
  2.  aa-genprof 喂流量 → 收敛 profile → aa-enforce
  3.  /etc/audit/rules.d/ 按上文规则最小集 → augenrules --load
  4.  auditd.conf 滚动与磁盘阈值 → syslog 联动
  5.  rsyslog 针对 apparmor="DENIED" 做转发
  6.  压测基线:记录 QPS、p95、CPU、日志增长率
  7.  版本化管理 apparmor.d 与 audit/rules.d
  8.  预案: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

如果你也在香港或任何一座城市的机房里熬夜守护业务,希望这套实践能帮你稳住边界、看清真相,让系统在黑夜里也保持从容。

目录结构
全文