在Ubuntu服务器上,Nginx默认配置下拥有读取自身配置和日志的权限,但一旦工作进程被攻击者通过漏洞控制,它就可能遍历整个文件系统,读取/etc/passwd、访问用户家目录下的敏感文件,甚至探测其他服务的配置文件。AppArmor的强制访问控制机制可以精确地将Nginx的文件访问范围锁定在几个必要的目录内,即使攻击者拿到了shell,也无法突破这个边界。

确认AppArmor运行状态并安装必要工具

Ubuntu 20.04及之后的版本默认启用AppArmor,内核启动参数中已包含安全模块。先检查当前状态:

sudo aa-status

输出中会显示已加载的策略数量、处于强制模式的配置文件列表。如果提示命令未找到,说明apparmor包未安装,执行:

sudo apt update && sudo apt install apparmor apparmor-utils apparmor-profiles -y

apparmor-utils提供了aa-genprof、aa-logprof、aa-complain等实用工具,这些是后续生成和调试策略的关键。安装完成后确认服务运行正常:

sudo systemctl status apparmor

Nginx本身也需要处于运行状态,且明确知道其可执行文件路径和版本。不同安装方式路径差异很大:官方源安装的nginx二进制通常在/usr/sbin/nginx,编译安装的可能在/usr/local/nginx/sbin/nginx。用which nginx或whereis nginx确认。

分析Nginx实际需要的目录和文件权限

在动手写策略之前,必须精确梳理Nginx运行所需的全部路径。一个典型的Nginx实例需要以下几类访问:

第一,二进制文件和库文件。Nginx主进程需要执行自身,加载libc、libpcre、libssl、libz等动态库。这些通常位于/usr/sbin/、/usr/lib/、/lib/目录下,权限只需读和执行。

第二,配置文件。主配置文件nginx.conf及其include引入的所有文件,通常位于/etc/nginx/目录。Nginx启动时读取,reload时重新读取,因此需要读权限。

第三,Web根目录。这是你希望限制Nginx访问的特定目录,比如/var/www/mysite/。Nginx需要读取静态文件,如果涉及上传功能,还需要对上传目录有写权限。关键点在于:只授予这个特定目录的访问权,而不是整个/var/www/。

第四,日志目录。access.log和error.log通常写入/var/log/nginx/,需要写权限。如果日志轮转使用了logrotate,还要考虑轮转后新文件的创建权限。

第五,运行时文件。pid文件通常位于/run/nginx.pid或/var/run/nginx.pid,需要读写。fastcgi、uwsgi等代理场景下还涉及Unix socket文件或TCP连接,AppArmor对网络访问的控制粒度较粗,主要关注文件系统。

第六,临时文件。Nginx处理大请求体时可能用到client_body_temp_path指定的目录,proxy缓存和fastcgi缓存也有对应目录。这些路径如果没有显式配置,默认在编译时确定,常见于/var/lib/nginx/或/var/cache/nginx/。

把这些路径列成清单,标注每个路径的权限需求:r(读)、w(写)、x(执行)、m(内存映射可执行)。

编写针对Nginx的AppArmor策略文件

AppArmor策略文件存放在/etc/apparmor.d/目录,命名规则通常是将可执行文件路径中的斜杠替换为点号。例如/usr/sbin/nginx的策略文件名为usr.sbin.nginx。创建这个文件:

sudo nano /etc/apparmor.d/usr.sbin.nginx

策略内容从最基本的框架开始,逐步添加规则。以下是一个限制Nginx仅访问/var/www/mysite/的完整示例:

#include <tunables/global>

/usr/sbin/nginx {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/openssl>

  # 允许执行自身
  /usr/sbin/nginx mr,

  # 动态库路径
  /usr/lib/x86_64-linux-gnu/.so* mr,
  /lib/x86_64-linux-gnu/.so* mr,

  # 配置文件
  /etc/nginx/ r,

  # 仅允许访问特定Web目录
  /var/www/mysite/ r,
  /var/www/mysite/ r,

  # 日志目录
  /var/log/nginx/ rw,

  # 运行时文件
  /run/nginx.pid rw,
  /run/nginx/ rw,
  /run/nginx/ rw,

  # 临时文件和缓存(按实际路径调整)
  /var/lib/nginx/ rw,
  /var/cache/nginx/ rw,

  # 拒绝其他所有文件访问
  deny / rwx,
}

这个策略有几个关键设计。第一行#include引入tunables全局变量定义。策略体以可执行文件路径开头,花括号内是规则列表。base抽象层包含了基础系统调用和/proc、/sys等伪文件系统的只读访问,nameservice抽象层允许DNS解析所需的/etc/resolv.conf等文件,openssl抽象层允许读取SSL证书和密钥相关文件。

mr权限表示允许内存映射并可执行,动态库必须用这个权限而不能只用r,否则加载器无法将库映射到进程地址空间。最后一条deny / rwx是显式拒绝规则,AppArmor默认就是白名单模式,未列出的路径自动拒绝,但显式写上能让意图更清晰,也便于审计日志中区分主动拒绝和隐式拒绝。

如果Nginx需要监听80和443端口,AppArmor默认允许网络操作,不需要额外规则。但如果你的策略中包含了显式的网络限制,需要添加:

  network inet stream,
  network inet6 stream,

这允许IPv4和IPv6的TCP流式套接字,对应HTTP和HTTPS通信。

将策略加载到内核并设置为强制模式

策略文件编写完成后,用apparmor_parser加载:

sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx

-r表示替换已存在的同名策略。然后将其设置为强制模式:

sudo aa-enforce /usr/sbin/nginx

这个命令会创建/etc/apparmor.d/disable/目录下的符号链接(如果存在兼容模式),并确保策略处于enforce状态。验证加载结果:

sudo aa-status | grep nginx

应该能看到/usr/sbin/nginx出现在enforce mode列表下。此时重启Nginx:

sudo systemctl restart nginx

如果Nginx启动失败,立即检查审计日志:

sudo journalctl -xe | grep -i apparmor

或者查看内核审计日志:

sudo tail -f /var/log/syslog | grep -i apparmor

日志中会出现类似"apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/some/path"的条目,明确指出被拒绝的路径和操作。根据这些信息补充缺失的路径规则。

使用aa-logprof根据实际运行日志微调策略

手动编写的策略几乎不可能一次性覆盖所有边缘情况。AppArmor提供了一套基于审计日志的半自动策略生成工具。先把策略切换到complain模式,让Nginx正常运行一段时间,记录所有本应被拒绝的操作但不实际阻止:

sudo aa-complain /usr/sbin/nginx

然后模拟正常业务流量:访问网站页面、触发各种HTTP方法、上传文件、生成错误日志等。尽可能覆盖所有功能路径。接着运行:

sudo aa-logprof

这个工具会扫描审计日志,逐条显示被拒绝的操作,并提供交互式选项:Allow(允许)、Deny(拒绝)、Glob(使用通配符)、Glob with extension等。对于每条记录,根据实际需求选择。比如日志中出现Nginx试图读取/var/www/mysite/.htaccess被拒绝,而你确实不需要Nginx读取隐藏文件,就选Deny。如果出现Nginx试图写入某个临时目录,就选Allow并确认路径模式。

aa-logprof会直接修改/etc/apparmor.d/usr.sbin.nginx文件,并在完成后自动重新加载策略。处理完所有条目后,把策略切回强制模式:

sudo aa-enforce /usr/sbin/nginx

这个迭代过程通常需要重复两到三轮,直到日志中不再出现与正常业务相关的拒绝记录。

验证限制效果:尝试突破边界

策略生效后,主动验证限制是否真正起作用。在服务器上模拟攻击者场景,用Nginx的工作进程权限尝试访问受限目录。最简单的方法是修改Nginx配置,添加一个返回文件内容的location块指向敏感文件,然后请求该路径。例如在nginx.conf中添加:

location /test-passwd {
    alias /etc/passwd;
}

重载Nginx后访问这个路径,浏览器会显示403或404,Nginx错误日志中记录"Permission denied",同时syslog中出现AppArmor的拒绝审计条目。这证明即使Nginx配置被篡改,文件系统级别的强制访问控制仍然阻止了越权读取。

更深入的验证可以通过strace附加到Nginx工作进程,观察系统调用返回值:

sudo strace -p <worker_pid> -e openat 2>&1 | grep -i eacces

当请求触发越权访问时,strace会捕获到openat系统调用返回EACCES (Permission denied),确认是AppArmor在内核层面拦截。

处理常见问题和进阶场景

PHP-FPM与Nginx配合时,AppArmor策略需要额外考虑。如果PHP-FPM以Unix socket方式与Nginx通信,socket文件通常位于/run/php/或/var/run/php/,Nginx策略中需要添加对该socket文件的读写权限:

/run/php/php*-fpm.sock rw,

如果PHP-FPM也配置了AppArmor策略,两个策略独立工作,各自限制各自进程的文件访问。这种纵深防御效果更好——即使攻击者通过Nginx漏洞进入了PHP-FPM进程,PHP-FPM自身的AppArmor策略会进一步限制其活动范围。

多个Web站点部署在同一台服务器时,可以为每个站点创建不同的Nginx实例或使用不同的AppArmor子配置文件。AppArmor支持在策略中定义子配置文件(child profile),但更简单的做法是运行多个Nginx容器或使用不同用户运行多个Nginx实例,每个实例绑定独立的AppArmor策略。如果必须在同一个Nginx进程中托管多站点,策略中的Web目录规则需要精确到每个站点的根目录,避免使用过宽的通配符。

Nginx版本升级后,二进制文件路径或依赖库可能变化。升级前先将策略切换到complain模式,升级后运行aa-logprof收集新的访问模式,确认无误后再切回enforce。这个流程可以纳入运维脚本自动化执行。

性能方面,AppArmor的路径匹配在内核中通过缓存机制高效执行,对Nginx的吞吐量和延迟影响通常在1%以内,几乎可以忽略。但策略规则数量过多(数千条)时,匹配开销会线性增长。保持策略精简,优先使用目录级通配符而非逐文件列出,能兼顾安全性和性能。

将AppArmor策略管理纳入配置管理和监控

生产环境中,AppArmor策略文件应该纳入版本控制,与Nginx配置文件一起管理。使用Ansible、Puppet或Chef等工具分发策略文件,并确保apparmor_parser -r命令在文件变更后自动执行。监控方面,配置日志告警规则,当AppArmor拒绝事件中包含特定关键词(如/etc/shadow、/root/)时触发安全告警,这可能是正在进行的攻击尝试。同时定期审计aa-status输出,确保关键服务的策略始终处于enforce模式,未被意外切换到complain或完全卸载。

AppArmor的策略语法相对SELinux简单很多,但功能足够强大。通过将Nginx的文件访问精确锁定到特定目录,你为服务器增加了一层内核级的防护,即使应用层出现未知漏洞,攻击者的横向移动空间也被极大压缩。这种限制不会影响正常业务,却能在关键时刻成为最后一道有效防线。