Nginx 配置模块化可以复用,但安全边界不能照搬

围绕 include 复用、CORS 白名单、try_files、TLS 1.2/1.3、HSTS、PHP-FastCGI 真实文件限制和 nginx -t 回滚,整理一份可直接落地的 Nginx 配置安全边界指南。

Nginx配置安全CORSTLSPHP-FastCGI

先说结论:Nginx 适合模块化,但安全配置不能拿来就抄

把 Nginx 拆成多个 include 片段,是很自然的做法:公共响应头放一处,PHP 入口放一处,SSL 参数放一处,站点自己的规则再单独放一处。问题不在于“能不能拆”,而在于“拆完以后,哪些写法还安全,哪些只是看起来简洁”。很多老教程的坑都出在这里:CORS 直接反射 $http_originif 用在 location 里做路由,PHP 只凭正则放行,TLS 参数停留在能连上就行的阶段。真正稳妥的做法,是把复用和边界一起设计。

一、先把可复用片段拆开,但不要把业务规则和安全规则混在一起

最常见的模块化方式,是把公共能力拆成独立文件,例如 CORS、PHP、TLS、站点入口和通用安全头。这样做的好处是:多个站点可以复用同一套策略,变更时也不会到处复制粘贴。坏处也很直接:一旦某个片段写错,所有站点一起中招。所以建议把“纯配置能力”和“站点个性化逻辑”分层。

http {
    include /etc/nginx/conf.d/mime.types;
    include /etc/nginx/snippets/cors.map.conf;
    include /etc/nginx/snippets/security-headers.conf;
    include /etc/nginx/snippets/php-fastcgi.conf;
}

这类拆法的目标不是炫技,而是把风险面缩小:全局只放共识规则,站点内只放必须知道域名、路径和入口文件的内容。

二、CORS 不要反射任意 Origin,白名单要放在 map

最危险的写法,是把请求头里的 Origin 原样回给浏览器,再顺手开上凭据。只要出现 Access-Control-Allow-Origin: $http_originAccess-Control-Allow-Credentials: true 的组合,就要立刻停手。这样等于允许任意站点借用户登录态跨站发请求,风险非常高。

更稳的做法是:在 http 级用 map 定义白名单,命中才返回允许的 Origin,没命中就留空;再配合 add_header ... always,保证 4xx/5xx 也带上你需要的安全头。预检请求也最好单独处理,不要交给 PHP 或框架绕一圈。

map $http_origin $cors_origin {
    default "";
    "https://app.example.com" $http_origin;
    "https://admin.example.com" $http_origin;
}

server {
    location /api/ {
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS always;
            add_header Access-Control-Allow-Headers Content-Type,Authorization always;
            add_header Content-Length 0;
            return 204;
        }

        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Access-Control-Allow-Credentials true always;
        add_header Vary Origin always;
    }
}

注意这里的重点不是“把头加上”,而是“只有白名单命中才加”。如果业务根本不需要跨站带凭据,就不要开 credentials

三、路由优先用 try_files,别把 ifrewrite 当万能胶

老式写法里常见这种判断:文件不存在就重写到入口文件。它在一些场景能工作,但可读性差,和 Nginx 的执行上下文也容易打架。Nginx 的 if 不是不能用,而是不要拿来做复杂流程控制,尤其不要把它当成应用路由引擎。

现代写法更直接:静态文件先找,找不到再落到入口。这样规则更短,行为更可预测,也更容易和框架配合。

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

如果是 ThinkPHP、Laravel、WordPress 这类入口清晰的应用,优先让框架的标准入口来接管,不要在 Nginx 里写一堆“猜路径”的 rewrite。

四、TLS 至少明确启用 1.2 和 1.3,HSTS 也要有边界

SSL/TLS 配置不要停留在“能访问就行”。首先,协议版本要显式写出来,不要把旧协议混进来;其次,站点应该明确走 HTTPS,避免浏览器在回退场景里继续尝试明文;最后,HSTS 不是越早越好,只有确认全站都能稳定 HTTPS,子域和回源链路也都没问题,才适合开启。

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

这里的 includeSubDomains 拼写不能错。HSTS 一旦发出去,浏览器会记住它;如果子域里还有没准备好的站点,后续排障会很麻烦。所以 HSTS 要当成一个“发布后的约束”,不是默认开关。

五、PHP / FastCGI 要限制真实文件,别让正则把解析面放大

PHP 相关配置最容易被忽略的点,是“哪些文件真的能交给 PHP 解释”。老教程里常把 location ~ \.php.*?$ 之类的写法直接搬上来,这会把 PATH_INFO、附加后缀和一些边界情况一起卷进来。更稳妥的方式,是只匹配真正的 .php 文件,并在交给 PHP-FPM 前先确认文件存在。

location ~ \.php$ {
    try_files $fastcgi_script_name =404;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

这里有两个关键点。第一,try_files 先挡住不存在的脚本,避免把任意路径送进 FastCGI。第二,SCRIPT_FILENAME$realpath_root 比简单拼接更稳,尤其是在软链接、发布目录切换、复杂 root 或 alias 场景里。

只有在应用确实需要 PATH_INFO 时,才考虑单独设计 split 规则,而且要把它当成例外,不要默认开启。

六、上线前先跑 nginx -t,回滚也要提前准备

安全配置的最后一步,不是“改完就重载”,而是先做语法和引用检查,再决定是否切流。nginx -t 能帮你尽早发现拼写错误、包含路径错位、证书路径不存在、变量名写错等问题。它不能证明业务逻辑完全正确,但至少能挡住最基础的配置事故。

nginx -t
systemctl reload nginx

如果你是在线上改配置,回滚策略也要提前写好。最简单的方式,是保留上一版配置备份,或者用版本控制管理 snippets/ 目录。建议在变更前后至少保留这三样东西:当前配置快照、可回退版本、以及本次变更的检查记录。这样一旦出现 502、CORS 失效或证书异常,回滚不会靠记忆。

七、一份更现代的组合示例

下面这组配置把上面几件事拼在一起:公共片段可复用,CORS 只对白名单开放,静态和入口通过 try_files 收口,PHP 只接真实文件,TLS 和 HSTS 明确声明。

http {
    map $http_origin $cors_origin {
        default "";
        "https://app.example.com" $http_origin;
    }

    server {
        listen 443 ssl http2;
        server_name example.com;

        ssl_protocols TLSv1.2 TLSv1.3;
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }

        location /api/ {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Vary Origin always;
        }

        location ~ \.php$ {
            try_files $fastcgi_script_name =404;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
            fastcgi_pass unix:/run/php/php-fpm.sock;
        }
    }
}

八、最后再自检一遍:复用可以,风险不要复用

这类 Nginx 配置最值得记住的一句话是:include 适合复用能力,不适合复用旧习惯。能复用的是白名单、固定头、TLS 基线、PHP 真实文件限制和回滚流程;不能直接复用的是任意 Origin 反射、宽松正则、危险 if、含糊的 SSL 参数和没验证过的重载动作。

如果你准备把配置拆成多个模块,建议每次只改一个边界,再跑一次 nginx -t,确认没问题再重载。安全配置不是“写得短”就算好,而是每个片段的职责都清楚,出问题时也知道该回哪一层。