一台 4 核 8G 的云服务器上托管了公司官网、客户的几个展示站、还有一个测试环境,成本确实省了。但总有隐隐的不安:万一其中一个站被挂马,会不会连累其他站?某个站点半夜跑批处理把 CPU 占满,会不会让所有站一起打不开?客户的安全扫描报告里“同一 IP 托管多站点”是不是风险项?
答案是:多站点同机本身不是安全问题,缺乏隔离才是。虚拟机、容器、云服务器本质上都是多租户共享硬件,区别在于隔离做得够不够。本文从四个维度展开——Web 层隔离、系统用户与权限隔离、资源配额隔离、数据与网络隔离——每一层都给出可直接落地的配置和明确的判断标准,并在最后给出“什么时候必须拆分成多台机器”的决策依据。
一、先认清同机多站的四类真实风险
| 风险类型 | 典型场景 | 后果 | 对应隔离手段 |
|---|---|---|---|
| 跨站入侵(连坐) | A 站有漏洞被上传 Webshell,攻击者读取同盘上的 B 站配置文件 | 数据库账号泄露、B 站被篡改 | 独立系统用户 + PHP-FPM 独立池 + open_basedir |
| 资源争抢 | C 站跑导出脚本占满 CPU/内存 | 所有站点响应变慢甚至 OOM 被杀 | FPM 进程数配额 + systemd/cgroup 限额 |
| 故障放大 | Nginx 配置一处语法错误 | 本机所有站点同时不可用 | 配置即代码 + 变更回滚机制 |
| 外部连坐 | 同 IP 上有违规站点被封、共用 IP 的邮件服务器被拉黑 | 你的站点被连带限流、发信被退 | 独立公网 IP、独立出口 IP |
这四类里,前两类是可以通过配置彻底解决的,第三类靠流程治理,第四类只能靠加 IP——下文逐个展开。
二、Web 层隔离:每个站点一个独立配置文件
最基础的一条纪律:不要把所有站点写在一个 nginx.conf 里。用 conf.d/ 拆分,一个站点一个文件,改错只影响自己,排查也方便。
2.1 目录约定
/etc/nginx/nginx.conf # 主配置,只放全局参数 /etc/nginx/conf.d/site-a.conf # 站点 A /etc/nginx/conf.d/site-b.conf # 站点 B /etc/nginx/snippets/php_fastcgi.conf # 公共 FastCGI 片段 /data/www/site-a/ # 站点 A 根目录,属主 site-a /data/www/site-b/ # 站点 B 根目录,属主 site-b /var/log/nginx/site-a.access.log /var/log/nginx/site-a.error.log
2.2 通用 server 块模板
server {
listen 80;
server_name www.site-a.com site-a.com;
root /data/www/site-a;
index index.php index.html;
access_log /var/log/nginx/site-a.access.log;
error_log /var/log/nginx/site-a.error.log;
client_max_body_size 20m; # 按站点需求单独设定,不用全局值
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/site-a.sock; # 关键:每个站点独立 socket
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_connect_timeout 5s;
fastcgi_read_timeout 60s;
}
location ~ /\. { deny all; } # 禁止访问 .git/.env/.svn
location ~* \.(jpg|jpeg|png|gif|css|js)$ { expires 30d; access_log off; }
}
2.3 防止恶意域名解析(必做)
Nginx 会把没有匹配到 server_name 的请求交给第一个 server 块处理。如果不设 default_server,别人把任意域名解析到你的 IP,就能白嫖你的站点内容(还可能被搜索引擎判为镜像站)。
# 兜底 server:放到 conf.d/00-default.conf,确保文件名排序最前
server {
listen 80 default_server;
listen 443 ssl default_server;
server_name _;
# 自签证书或直接拒绝,避免 Nginx 报错
ssl_certificate /etc/nginx/ssl/dummy.crt;
ssl_certificate_key /etc/nginx/ssl/dummy.key;
return 444; # 直接断开连接,不返回任何内容
}
三、系统用户隔离:一个站点一个用户
这是防止跨站入侵的核心。原则很简单:站点 A 的进程不能有权限读站点 B 的文件。做法是给每个站点创建独立的系统用户,并让对应的 PHP-FPM 池以该用户身份运行。
3.1 创建站点专用用户
# 创建不可登录的系统用户,家目录即站点根目录
useradd -r -s /sbin/nologin -d /data/www/site-a -M site-a
useradd -r -s /sbin/nologin -d /data/www/site-b -M site-b
# 确认没有 shell 登录权限
grep -E 'site-a|site-b' /etc/passwd
# site-a:x:1001:1001::/data/www/site-a:/sbin/nologin
# 目录属主与权限:750 意味着其他用户完全无法进入
chown -R site-a:site-a /data/www/site-a
find /data/www/site-a -type d -exec chmod 750 {} \;
find /data/www/site-a -type f -exec chmod 640 {} \;
# 需要 Web 读取的资源放宽
chmod -R 755 /data/www/site-a/static
3.2 为每个站点建立独立的 PHP-FPM 池
# /etc/php/8.1/fpm/pool.d/site-a.conf [site-a] user = site-a group = site-a listen = /run/php/site-a.sock listen.owner = nginx listen.group = nginx listen.mode = 0660 pm = dynamic pm.max_children = 20 # 按配额分配,见第五节 pm.start_servers = 5 pm.min_spare_servers = 3 pm.max_spare_servers = 8 pm.max_requests = 500 request_terminate_timeout = 30s request_slowlog_timeout = 3s slowlog = /var/log/php/site-a.slow.log # 关键:限制该池只能访问自己的目录 php_admin_value[open_basedir] = /data/www/site-a/:/tmp/:/var/tmp/ php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen,putenv php_admin_flag[allow_url_fopen] = off # 若存在第二个池,注意 pm.status_path 要不同 pm.status_path = /fpm-status-site-a
# 校验配置并重载
php-fpm8.1 -t
systemctl reload php8.1-fpm
# 验证 socket 归属与权限
ls -l /run/php/site-a.sock
# srw-rw---- 1 nginx nginx ...
# 验证 PHP 进程的运行用户确实是 site-a
ps aux | grep 'php-fpm: pool site-a' | grep -v grep | awk '{print $1}' | sort -u
3.3 常见权限模型对比
| 模型 | 做法 | 安全性 | 适用场景 |
|---|---|---|---|
| 全部 www-data 单用户 | 所有站点共用一个用户 | 低:一个站沦陷全部沦陷 | 不推荐,仅限个人测试 |
| 独立用户 + 独立 FPM 池 | 本文推荐方案 | 高:进程级别隔离 | 多站点托管标配 |
| 容器隔离 | 每个站点一个容器 | 高:文件系统 + 进程 + 网络均可隔离 | 需要更强的资源/版本隔离 |
| 虚拟机隔离 | 每台虚拟机一个站 | 最高:内核级隔离 | 合规要求严格、互相不信任的多方业务 |
3.4 Nginx FastCGI 侧的配套防护
# 防止 PHP 文件解析漏洞(如 /upload/a.jpg/a.php)
location ~ \.php$ {
try_files $uri =404; # 文件不存在直接 404,不让 FPM 猜路径
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/site-a.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# php.ini 里同时关闭 pathinfo 修正
cgi.fix_pathinfo = 0
# 上传目录禁止执行 PHP
location ^~ /upload/ {
location ~ \.php$ { deny all; }
}
四、资源分配:避免一个站拖垮全部
权限隔离管的是安全,资源限额管的是可用性。两者都要做。
4.1 按内存配额反推 FPM 进程数
# 计算总预算:假设 8G 内存机器,托管 3 个站,预留 2G 给系统+MySQL+Nginx 可用给 PHP 的内存 ≈ 4GB # 分配方案(按站点重要性) 站点 A(核心): 2GB → 单进程 40MB → pm.max_children = 2048/40 ≈ 50 站点 B(一般): 1.5GB → 单进程 40MB → pm.max_children ≈ 38 站点 C(测试): 0.5GB → 单进程 40MB → pm.max_children ≈ 12
把算出来的值分别写进三个 FPM 池。这样即使站点 C 跑死,它也最多占用自己那份进程数,不会抢走 A 和 B 的资源。
4.2 用 systemd 做更硬的限额
如果想连 CPU 也限制住,可以把大流量/不可信的站点放到独立的 PHP-FPM 服务实例,再用 systemd 配额:
# 复制一份独立 FPM 服务(Ubuntu 下 php8.1-fpm 支持多实例) cp /lib/systemd/system/php8.1-fpm.service /etc/systemd/system/php-fpm-sitec.service systemctl daemon-reload systemctl enable --now php-fpm-sitec # 设置 CPU 与内存上限 systemctl set-property php-fpm-sitec.service CPUQuota=50% MemoryMax=1G # 持久化写入 drop-in 文件 /etc/systemd/system.control/php-fpm-sitec.d/*.conf systemctl show php-fpm-sitec | grep -E 'CPUQuota|MemoryMax'
CPUQuota=50% 表示最多使用一个核的 50%(200% 即两个核)。MemoryMax 达到上限后该服务会被限流或杀进程,但不会波及其他服务。
4.3 Nginx 层的限流与连接控制
# http 段定义
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_req_zone $binary_remote_addr zone=siteb:10m rate=10r/s;
# 站点 B 的 server/location 里应用
server {
server_name www.site-b.com;
limit_conn perip 20; # 单 IP 最多 20 个并发连接
location / {
limit_req zone=siteb burst=20 nodelay; # 单 IP 10 req/s,突发 20
try_files $uri $uri/ /index.php?$query_string;
}
}
4.4 磁盘与 inode 配额
# 查看某站点占用 du -sh /data/www/site-a df -i # inode 耗尽同样会让站点无法写入 # XFS 项目配额(CentOS/RHEL) xfs_quota -x -c 'project -s -p /data/www/site-a 100' /data xfs_quota -x -c 'limit -p bsoft=18g bhard=20g 100' /data xfs_quota -x -c 'report -h' /data
五、数据与网络层面的隔离
5.1 数据库必须一库一账号
# 每个站点独立库 + 独立账号,只授权自己的库 CREATE DATABASE site_a DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'sa'@'localhost' IDENTIFIED BY '强密码A'; GRANT ALL PRIVILEGES ON site_a.* TO 'sa'@'localhost'; FLUSH PRIVILEGES; # 绝不使用 root 连接业务库 mysql -e "SELECT user,host FROM mysql.user;" mysql -e "SHOW GRANTS FOR 'sa'@'localhost';" # 确认 MySQL 只监听本地,不被外部直连 grep -nE 'bind-address|skip-networking' /etc/my.cnf ss -lntp | grep 3306 # 正确结果应为 127.0.0.1:3306 或仅 socket
5.2 Redis 也要隔离
- 不要让多个站点共用一个 Redis 库且无密码:设置
requirepass,或按业务分配不同 db index(SELECT 0/1/2),但更推荐实例分离。 - 给不同站点用不同的 key 前缀,避免互相覆盖。
- Redis 禁止监听 0.0.0.0,加
rename-command FLUSHALL ""等危险命令重命名。
5.3 公网 IP 与邮件出口
| 关注点 | 同 IP 影响 | 建议 |
|---|---|---|
| 搜索引擎 | 一般无影响,搜索引擎按域名而非 IP 收录 | 正常托管即可 |
| HTTPS | SNI 已普及,多证书共存无问题 | 确保每站配置正确的 server_name 与证书 |
| HTTP/2 | 同一连接可复用,多站共享无影响 | 无需处理 |
| 邮件发送 | 同 IP 有人发垃圾邮件会导致你的退信率飙升 | 使用第三方 SMTP 中继(独立发信域名);业务邮件不要自建 |
| 合规审查 | 等保/客户尽调可能要求业务隔离 | 合规场景建议拆机或用独立 VPC |
六、什么时候必须拆分成多台服务器
给出明确的判断标准,避免凭感觉决策:
- 安全等级不同:涉及支付、用户隐私数据的业务,不要和展示型站点、测试环境混部。
- 资源已无余量:高峰期 load 长期高于核数 × 2,或可用内存长期低于 20%,加更多站点只会互相拖累。
- 软件版本冲突:一个站要 PHP 5.6、另一个要 PHP 8.1,同机维护成本极高,应当拆开或用容器。
- 合规要求:客户合同或等级保护测评明确要求物理/逻辑隔离。
- 故障影响面不可接受:核心业务停机 1 分钟损失巨大,就不该与任何第三方站点共享机器。
- SLA 要求不同:核心站要求 99.9%,测试站随便重启,两者混部必然妥协。
七、多站点部署实施清单
- 规划目录与命名:
/data/www/<站点>,一个站点一个 conf.d 配置文件。 - 为每个站点创建独立不可登录用户,主目录权限 750。
- 为每个站点建立独立 PHP-FPM 池,池用户即站点用户,socket 独立命名。
- 配置
open_basedir限制文件访问范围,配置disable_functions禁用危险函数。 - Nginx server 块加
try_files $uri =404,上传目录禁止 PHP 执行。 - 设置 default_server 返回 444,防止恶意域名解析。
- 关闭 PHP
cgi.fix_pathinfo。 - 为每个站点创建独立数据库和独立账号,禁止使用 root 连接。
- 按内存预算反推每个池的
pm.max_children,总和不超载。 - 为资源敏感站点配置 systemd CPUQuota / MemoryMax 或容器限额。
- 每站点独立 access_log / error_log,配置 logrotate 轮转。
- 每站点独立备份任务与保留策略,定期验证可恢复。
- 配置 fail2ban、SSH 禁密码登录、仅开放必要端口。
- 建立变更流程:改配置先
nginx -t,保留回滚版本。
小结
多台网站部署在同一台服务器上完全可以做到安全可控,前提是完成四层隔离:用户层(独立系统用户 + 独立 FPM 池)、文件层(open_basedir + 目录权限)、资源层(进程数配额 + systemd/cgroup 限额)、数据层(独立库账号 + Redis 隔离)。做完这四层,单个站点被入侵或跑飞,影响范围就被限制在自己那一亩三分地内。
避坑清单:
- 永远不要让所有站点共用一个 Web 用户,这是最致命也最常见的错误。
- 不要给任何目录 777,也不要让站点 A 的用户能读站点 B 的配置文件。
- 别忘设
default_server,否则会被恶意解析白嫖流量。 pm.max_children必须按“站点配额”分配而不是所有池都用同一个大数字,否则内存照样会爆。- 多个池的
slowlog、socket、pm.status_path都不能重名,否则会互相覆盖。 - 测试环境和生产绝不同机,很多事故都源于“临时借用生产机跑个测试”。
- 业务邮件走第三方中继,别让同 IP 的其他站影响你的发信信誉。
- 备份必须逐站点独立配置,共用备份脚本时务必验证每个站点都能单独恢复。
关键词回顾:多网站部署同一服务器、站点隔离、PHP-FPM 独立池、open_basedir、独立系统用户、资源配额、独立数据库账号、安全配置。
企业QQ咨询




