1. PHP SAPI架构概述
1.1 什么是SAPI
定义: SAPI (Server Application Programming Interface) 是PHP与外部环境交互的抽象层。它定义了PHP如何接收输入、发送输出以及访问服务器资源。
通俗类比: 把SAPI想象成PHP的"插头适配器":
- CLI SAPI: 直接连接命令行终端
- FPM SAPI: 通过FastCGI协议与Web服务器通信
- Apache SAPI: 作为Apache模块直接嵌入
主要SAPI类型:
| SAPI | 适用场景 | 进程模型 | 安全特点 |
|---|---|---|---|
| CLI | 命令行脚本、Cron任务 | 单次执行 | 完整系统访问权限 |
| FPM | 生产Web应用 | 进程池 | 独立用户、资源隔离 |
| Apache2Handler | Apache集成 | 模块共享 | 继承Apache权限 |
| CGI | 传统兼容 | 每次请求fork | 高隔离、低性能 |
| Embed | 嵌入式应用 | 依赖宿主 | 由宿主程序控制 |
1.2 SAPI的底层差异
c
// SAPI结构简化的伪代码表示
struct sapi_module_struct {
char *name; // SAPI名称
int (*startup)(void); // 启动函数
int (*shutdown)(void); // 关闭函数
int (*activate)(void); // 请求开始
int (*deactivate)(void); // 请求结束
void (*input_filter)(...); // 输入过滤
void (*output_filter)(...); // 输出过滤
};
不同SAPI的关键差异:
-
生命周期管理
- CLI: 脚本级生命周期
- FPM: 请求级生命周期,进程复用
- Apache模块: 服务器级生命周期
-
输入输出机制
- CLI: 直接访问stdin/stdout/stderr
- FPM: 通过FastCGI协议传输
- Apache: 使用Apache的request_rec结构
-
环境变量访问
- CLI: 直接访问shell环境
- Web SAPI: 通过HTTP头和服务器的CGI变量
1.3 检测当前SAPI
php
<?php
/**
* SAPI检测与适配
*/
class SapiDetector {
/**
* 获取当前SAPI名称
*/
public static function getName(): string {
return php_sapi_name();
}
/**
* 检测是否为CLI模式
*/
public static function isCli(): bool {
return PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg';
}
/**
* 检测是否为Web SAPI
*/
public static function isWeb(): bool {
return !self::isCli();
}
/**
* 检测是否为FPM
*/
public static function isFpm(): bool {
return PHP_SAPI === 'fpm-fcgi';
}
/**
* 检测是否为Apache模块
*/
public static function isApache(): bool {
return strpos(PHP_SAPI, 'apache') !== false;
}
/**
* 获取详细的SAPI信息
*/
public static function getInfo(): array {
return [
'sapi_name' => PHP_SAPI,
'php_version' => PHP_VERSION,
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown',
'is_cli' => self::isCli(),
'is_fpm' => self::isFpm(),
'is_apache' => self::isApache(),
];
}
}
// 使用示例
if (SapiDetector::isCli()) {
echo "Running in CLI mode\n";
} else {
echo "Running in web mode: " . SapiDetector::getName() . "\n";
}
?>
2. CLI模式安全特性
2.1 CLI的独特能力
CLI模式拥有PHP最完整的系统访问能力,这也意味着最高的安全风险。
php
<?php
/**
* CLI特有功能与安全风险
*/
if (PHP_SAPI !== 'cli') {
die("This script must be run from CLI\n");
}
// 1. 进程控制函数 (仅在CLI可用)
$pid = pcntl_fork(); // 创建子进程
if ($pid === -1) {
die("Failed to fork\n");
} elseif ($pid === 0) {
// 子进程
echo "Child process\n";
exit(0);
} else {
// 父进程
pcntl_waitpid($pid, $status);
echo "Child exited with status $status\n";
}
// 2. 信号处理
pcntl_signal(SIGINT, function($signo) {
echo "Caught SIGINT\n";
exit(1);
});
// 3. 直接访问stdin/stdout/stderr
$input = fgets(STDIN);
fwrite(STDOUT, "Output: $input");
fwrite(STDERR, "Error log\n");
// 4. 获取/设置进程标题
cli_set_process_title("MyWorkerProcess");
echo "PID: " . getmypid() . "\n";
echo "Parent PID: " . posix_getppid() . "\n";
// 5. 直接执行系统命令 (危险!)
// system("rm -rf /"); // 永远不要这样做
// 安全的命令执行
$safeCommand = escapeshellarg($userInput);
$output = shell_exec("ls -la $safeCommand 2>&1");
?>
CLI专属函数:
| 函数 | 功能 | 安全风险 |
|---|---|---|
pcntl_* |
进程控制 | 资源耗尽、进程注入 |
posix_* |
POSIX函数 | 权限提升、信息泄露 |
proc_open |
进程管道 | 命令执行 |
system/exec |
系统命令 | 任意代码执行 |
passthru |
直接输出 | 命令注入 |
ttyname |
终端名称 | 信息泄露 |
2.2 CLI脚本安全开发
php
#!/usr/bin/env php
<?php
/**
* 安全的CLI脚本模板
*/
// 1. 严格的SAPI检查
if (PHP_SAPI !== 'cli') {
fwrite(STDERR, "Error: CLI only\n");
exit(1);
}
// 2. 设置安全的错误处理
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('log_errors', '1');
// 3. 限制执行时间(防止无限循环)
set_time_limit(300); // 5分钟
// 4. 限制内存使用
ini_set('memory_limit', '256M');
// 5. 参数解析
$options = getopt('u:p:h', ['user:', 'password:', 'help']);
if (isset($options['h']) || isset($options['help'])) {
echo "Usage: script.php -u <user> -p <password>\n";
echo "Options:\n";
echo " -u, --user Username\n";
echo " -p, --password Password\n";
echo " -h, --help Show this help\n";
exit(0);
}
// 6. 验证必需参数
$user = $options['u'] ?? $options['user'] ?? null;
$password = $options['p'] ?? $options['password'] ?? null;
if (!$user || !$password) {
fwrite(STDERR, "Error: Missing required parameters\n");
exit(1);
}
// 7. 清理输入
$user = escapeshellarg($user);
// 8. 信号处理(优雅退出)
$running = true;
pcntl_signal(SIGTERM, function() use (&$running) {
$running = false;
echo "Received SIGTERM, shutting down...\n";
});
pcntl_signal(SIGINT, function() use (&$running) {
$running = false;
echo "Received SIGINT, shutting down...\n";
});
// 9. 主逻辑
try {
while ($running) {
pcntl_signal_dispatch();
// 处理任务
processTask($user);
sleep(1);
}
} catch (Exception $e) {
fwrite(STDERR, "Fatal error: " . $e->getMessage() . "\n");
exit(1);
}
exit(0);
function processTask(string $user): void {
echo "Processing for user: $user\n";
}
?>
2.3 Cron任务安全
php
<?php
/**
* 安全的Cron任务脚本
*/
class SecureCronJob {
private $lockFile;
private $logFile;
public function __construct(string $jobName) {
$this->lockFile = "/var/run/cron_{$jobName}.lock";
$this->logFile = "/var/log/cron/{$jobName}.log";
}
/**
* 防止并发执行
*/
public function acquireLock(): bool {
$fp = fopen($this->lockFile, 'c');
if (!$fp) {
throw new RuntimeException("Cannot create lock file");
}
if (!flock($fp, LOCK_EX | LOCK_NB)) {
fclose($fp);
return false; // 另一个实例正在运行
}
// 保持文件打开以维持锁
return true;
}
/**
* 记录日志
*/
public function log(string $message, string $level = 'INFO'): void {
$line = sprintf(
"[%s] [%s] %s\n",
date('Y-m-d H:i:s'),
$level,
$message
);
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
/**
* 执行作业
*/
public function run(callable $job): void {
if (!$this->acquireLock()) {
$this->log("Job already running, skipping", 'WARNING');
return;
}
$startTime = microtime(true);
try {
$this->log('Job started');
$job();
$this->log('Job completed');
} catch (Exception $e) {
$this->log("Job failed: {$e->getMessage()}", 'ERROR');
exit(1);
} finally {
$duration = microtime(true) - $startTime;
$this->log("Duration: {$duration}s");
}
}
}
// 使用
$cron = new SecureCronJob('daily_report');
$cron->run(function() {
// 执行报表生成
generateReport();
});
?>
3. PHP-FPM安全架构
3.1 FPM进程池配置
PHP-FPM通过进程池实现资源隔离和权限控制。
ini
; =====================================================
; FPM进程池安全配置
; =====================================================
[global]
; 错误日志
error_log = /var/log/php-fpm/error.log
; 进程ID文件
pid = /var/run/php-fpm.pid
; 紧急重启阈值(内存溢出时)
emergency_restart_threshold = 10
emergency_restart_interval = 1m
; 进程控制超时
process_control_timeout = 10s
; 限制FPM可以生成的进程数
process.max = 128
; 运行FPM的系统用户(仅启动时有效)
; user = www-data
; group = www-data
; 守护进程化
daemonize = yes
[www]
; 监听方式
; Unix socket更安全(无法远程访问)
listen = /var/run/php-fpm/www.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; TCP方式(需要防火墙保护)
; listen = 127.0.0.1:9000
; listen.allowed_clients = 127.0.0.1
; 进程池管理
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500 ; 防止内存泄漏
; 进程用户(重要安全设置)
user = www-data
group = www-data
; 请求限制
request_terminate_timeout = 30s
request_slowlog_timeout = 10s
slowlog = /var/log/php-fpm/slow.log
; 安全限制
; 强制php.ini中的限制不能被覆盖
php_admin_value[memory_limit] = 128M
php_admin_value[max_execution_time] = 30
php_admin_value[open_basedir] = /var/www/html:/tmp
php_admin_flag[display_errors] = off
; 环境变量清理
clear_env = yes
; 只允许特定环境变量
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp
; 访问日志(调试用,生产关闭)
; access.log = /var/log/php-fpm/access.log
3.2 多站点隔离配置
ini
; =====================================================
; 多站点FPM隔离配置
; =====================================================
; 站点1: www.site1.com
[site1]
listen = /var/run/php-fpm/site1.sock
listen.owner = www-data
listen.group = webserver
listen.mode = 0660
user = site1
group = site1
pm = dynamic
pm.max_children = 20
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 10
; 站点特定的限制
php_admin_value[open_basedir] = /var/www/site1:/tmp:/var/log/site1
php_admin_value[upload_tmp_dir] = /var/www/site1/tmp
php_admin_value[session.save_path] = /var/www/site1/sessions
; 站点1不能访问站点2的文件
; 通过Linux权限进一步限制
chroot = /var/www/site1
; 站点2: www.site2.com
[site2]
listen = /var/run/php-fpm/site2.sock
listen.owner = www-data
listen.group = webserver
listen.mode = 0660
user = site2
group = site2
pm = dynamic
pm.max_children = 20
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 10
php_admin_value[open_basedir] = /var/www/site2:/tmp:/var/log/site2
php_admin_value[upload_tmp_dir] = /var/www/site2/tmp
php_admin_value[session.save_path] = /var/www/site2/sessions
3.3 FPM安全监控
php
<?php
/**
* FPM状态监控与安全检测
*/
class FpmMonitor {
private $statusUrl = 'http://localhost/fpm-status';
/**
* 获取FPM状态
*/
public function getStatus(): ?array {
$json = file_get_contents($this->statusUrl . '?json');
return $json ? json_decode($json, true) : null;
}
/**
* 检测异常活动
*/
public function detectAnomalies(): array {
$status = $this->getStatus();
if (!$status) {
return ['error' => 'Cannot fetch status'];
}
$alerts = [];
// 检测高负载
if ($status['active-processes'] > $status['max-children'] * 0.8) {
$alerts[] = [
'level' => 'warning',
'message' => 'High process usage: ' . $status['active-processes'] . '/' . $status['max-children']
];
}
// 检测慢请求
if ($status['slow-requests'] > 10) {
$alerts[] = [
'level' => 'warning',
'message' => 'Slow requests detected: ' . $status['slow-requests']
];
}
// 检测最大子进程数限制
if ($status['max-children-reached'] > 0) {
$alerts[] = [
'level' => 'critical',
'message' => 'Max children reached ' . $status['max-children-reached'] . ' times'
];
}
// 检测进程重启次数(可能内存泄漏)
foreach ($status['processes'] as $process) {
if ($process['requests'] > 1000) {
$alerts[] = [
'level' => 'info',
'message' => 'Process ' . $process['pid'] . ' has handled ' . $process['requests'] . ' requests'
];
}
}
return $alerts;
}
/**
* 获取进程详情
*/
public function getProcessDetails(): array {
$status = $this->getStatus();
return $status['processes'] ?? [];
}
}
// 命令行监控
if (PHP_SAPI === 'cli') {
$monitor = new FpmMonitor();
echo "FPM Status Check\n";
echo str_repeat('=', 50) . "\n";
$anomalies = $monitor->detectAnomalies();
if (empty($anomalies)) {
echo "No anomalies detected.\n";
} else {
foreach ($anomalies as $alert) {
echo "[{$alert['level']}] {$alert['message']}\n";
}
}
}
?>
4. Apache模块模式
4.1 Apache模块的安全特点
Apache模块模式将PHP嵌入Apache进程中,具有独特的安全特性。
apache
# =====================================================
# Apache PHP模块安全配置
# =====================================================
# 加载PHP模块
LoadModule php_module modules/libphp.so
# 添加PHP处理器
AddHandler php-script .php
AddType application/x-httpd-php .php
# 仅在需要时启用(安全最佳实践)
<FilesMatch "\.(php|phar)$">
SetHandler application/x-httpd-php
</FilesMatch>
# 禁用危险的PHP文件执行
<Directory />
php_flag engine off
</Directory>
<Directory "/var/www/html">
php_flag engine on
# 覆盖php.ini设置
php_flag display_errors off
php_value memory_limit 128M
php_value max_execution_time 30
php_value upload_max_filesize 2M
# 禁用危险函数
php_admin_value disable_functions "exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec"
# 设置open_basedir
php_admin_value open_basedir "/var/www/html:/tmp:/var/log/php"
</Directory>
# 特定目录额外限制
<Directory "/var/www/html/uploads">
# 禁止执行PHP
php_flag engine off
# 或者限制PHP功能
php_value upload_max_filesize 0
</Directory>
4.2 Apache与FPM对比
apache
# =====================================================
# Apache + PHP-FPM配置(推荐用于生产环境)
# =====================================================
# 禁用模块PHP(如果使用FPM)
# LoadModule php_module modules/libphp.so
# 配置ProxyPass到FPM
<FilesMatch "\.(php|phar)$">
SetHandler "proxy:unix:/var/run/php-fpm/www.sock|fcgi://localhost"
</FilesMatch>
# 优势:
# 1. FPM进程崩溃不影响Apache
# 2. 每个站点可以运行不同的用户
# 3. 更好的资源隔离
# 4. 支持更多FPM功能(慢日志、状态页等)
# 额外的安全头
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
4.3 .htaccess安全考虑
apache
# =====================================================
# .htaccess安全配置
# =====================================================
# 在Apache主配置中限制.htaccess覆盖能力
<Directory />
# 禁止所有.htaccess
AllowOverride None
</Directory>
<Directory "/var/www/html">
# 允许特定覆盖
AllowOverride FileInfo Indexes Limit
# 绝不允许覆盖:
# - Options (可能启用危险的选项如ExecCGI)
# - AuthConfig (可能被用于密码保护绕过)
</Directory>
# 在.htaccess中可安全使用的配置
# .htaccess内容示例:
# 启用压缩
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml
</IfModule>
# 设置缓存
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 week"
</IfModule>
# PHP配置(仅在AllowOverride包含Options或FileInfo时允许)
php_flag display_errors off
php_value memory_limit 64M
# 注意:以下配置如果被攻击者控制.htaccess,将非常危险
# php_flag engine on <- 攻击者可以上传PHP文件并执行
# php_value auto_prepend_file /path/to/shell.php
安全检查清单:
- 🔴 生产环境应禁用
.htaccess(AllowOverride None) - 🔴 禁止通过
.htaccess启用engine(防止上传目录执行) - 🟢 所有配置应集中在主配置文件中
- 🟢 使用
apachectl -S检查配置语法
5. 其他SAPI模式
5.1 CGI模式
apache
# CGI模式配置(不推荐用于生产)
ScriptAlias /cgi-bin/ "/usr/local/cgi-bin/"
<Directory "/usr/local/cgi-bin">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Require all granted
# 限制CGI只能执行特定程序
<FilesMatch "^php-cgi$">
Require all granted
</FilesMatch>
<FilesMatch ".*">
Require all denied
</FilesMatch>
</Directory>
# 设置CGI环境变量
SetEnv PHPRC /var/www/conf
CGI安全特点:
- ✅ 每次请求都fork新进程,隔离性好
- ✅ 进程结束后资源完全释放
- ❌ 性能最差
- ❌ 仍然存在CVE-2012-1823等历史漏洞
5.2 Embed SAPI
c
// PHP Embed SAPI使用示例
#include <sapi/embed/php_embed.h>
int main(int argc, char *argv[]) {
PHP_EMBED_START_BLOCK(argc, argv)
// 执行PHP代码
zend_eval_string("echo 'Hello from embedded PHP';", NULL, "Embed test");
PHP_EMBED_END_BLOCK()
return 0;
}
Embed安全考虑:
- 宿主导程序控制所有资源
- PHP配置由宿主程序决定
- 适合桌面应用和嵌入式系统
5.3 LiteSpeed SAPI
ini
; LiteSpeed SAPI配置
[litespeed]
listen = /tmp/lshttpd/lsphp.sock
user = nobody
group = nobody
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
LiteSpeed特点:
- 与FPM类似的进程管理
- 更好的事件驱动性能
- 与LiteSpeed Web服务器深度集成
6. SAPI安全对比与选型
6.1 安全特性对比表
| 特性 | CLI | FPM | Apache模块 | CGI |
|---|---|---|---|---|
| 进程隔离 | 独立 | 进程池 | 共享 | 每次请求 |
| 内存隔离 | 完全 | 进程级 | 共享 | 完全 |
| 崩溃影响 | 单个脚本 | 单个请求 | 整个服务器 | 单个请求 |
| 权限控制 | 运行用户 | 池配置 | Apache用户 | CGI用户 |
| 资源限制 | 代码级 | 池配置 | 代码级 | 系统级 |
| 安全风险 | 最高 | 中等 | 中等 | 较低 |
| 性能 | N/A | 高 | 高 | 低 |
6.2 场景化选型建议
场景1: 生产Web应用
- 推荐: PHP-FPM + Nginx/Apache
- 原因 :
- 进程隔离,单个请求崩溃不影响其他请求
- 可以按站点设置不同用户
- 支持慢日志、状态监控
- 资源控制更精细
场景2: 共享主机环境
- 推荐: PHP-FPM (每个用户独立池)
- 或者: suPHP/FastCGI (已废弃)
- 避免: Apache模块(所有用户共享同一进程)
场景3: 内部工具/管理后台
- 推荐: PHP-FPM + 额外的访问控制
- 考虑: Apache模块(简单部署)
场景4: 命令行脚本
- 唯一选择: CLI
- 安全措施 :
- 运行专用低权限用户
- 使用文件锁防止并发
- 设置执行时间限制
场景5: 嵌入式应用
- 推荐: Embed SAPI
- 原因: 宿主导程序完全控制
6.3 SAPI检测与适配代码
php
<?php
/**
* SAPI感知的安全基类
*/
abstract class SapiAwareBase {
/**
* 根据SAPI应用安全限制
*/
protected function applySapiSecurity(): void {
switch (PHP_SAPI) {
case 'cli':
$this->applyCliSecurity();
break;
case 'fpm-fcgi':
$this->applyFpmSecurity();
break;
case 'apache2handler':
$this->applyApacheSecurity();
break;
default:
$this->applyDefaultSecurity();
}
}
/**
* CLI模式安全设置
*/
private function applyCliSecurity(): void {
// CLI允许更长的执行时间
set_time_limit(300);
// 但限制内存
ini_set('memory_limit', '512M');
// 禁用Web相关功能
if (function_exists('header_remove')) {
header_remove();
}
// 检查是否在正确的目录运行
$cwd = getcwd();
if (!str_starts_with($cwd, '/var/www/scripts')) {
throw new SecurityException("Script must run from /var/www/scripts");
}
}
/**
* FPM模式安全设置
*/
private function applyFpmSecurity(): void {
// FPM由池配置控制,代码只做补充
// 确保open_basedir已设置
if (empty(ini_get('open_basedir'))) {
error_log("Warning: open_basedir not set in FPM pool");
}
// 检查运行用户
$user = posix_getpwuid(posix_geteuid());
if ($user['name'] === 'root') {
throw new SecurityException("PHP should not run as root");
}
}
/**
* Apache模块安全设置
*/
private function applyApacheSecurity(): void {
// Apache模块共享进程,需要特别小心
// 清理环境变量
foreach ($_ENV as $key => $value) {
if (!in_array($key, ['PATH', 'TMPDIR', 'HOSTNAME'])) {
putenv("$key");
unset($_ENV[$key]);
}
}
// 设置严格的内存限制
ini_set('memory_limit', '128M');
}
/**
* 默认安全设置
*/
private function applyDefaultSecurity(): void {
// 未知SAPI,应用最严格的限制
ini_set('disable_functions', 'exec,shell_exec,system,passthru');
ini_set('allow_url_fopen', '0');
}
}
/**
* 应用初始化类
*/
class SecureApplication extends SapiAwareBase {
public function init(): void {
$this->applySapiSecurity();
// 其他初始化...
}
}
// 使用
$app = new SecureApplication();
$app->init();
?>
7. 运行环境加固
7.1 文件系统权限
bash
#!/bin/bash
# PHP运行环境权限加固脚本
# 设置PHP安装目录权限
chown -R root:root /usr/local/php
chmod 755 /usr/local/php/bin/php
chmod 755 /usr/local/php/sbin/php-fpm
# 设置配置文件权限
chown root:root /usr/local/php/etc/php.ini
chmod 644 /usr/local/php/etc/php.ini
# 设置FPM配置权限
chown root:root /usr/local/php/etc/php-fpm.d/
chmod 755 /usr/local/php/etc/php-fpm.d/
# 创建并设置日志目录
mkdir -p /var/log/php
chown www-data:www-data /var/log/php
chmod 750 /var/log/php
# 设置会话目录
mkdir -p /var/www/sessions
chown www-data:www-data /var/www/sessions
chmod 700 /var/www/sessions
# 设置上传临时目录
mkdir -p /var/www/tmp
chown www-data:www-data /var/www/tmp
chmod 700 /var/www/tmp
# 设置Web根目录
chown -R root:root /var/www/html
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
# 上传目录可写但不可执行
chown www-data:www-data /var/www/html/uploads
chmod 733 /var/www/html/uploads
7.2 Linux能力(Capabilities)
bash
# 使用Linux capabilities限制PHP权限
# 即使PHP进程被入侵,也无法执行某些特权操作
# 查看当前能力
getcap /usr/local/php/sbin/php-fpm
# 移除不必要的能力
setcap -r /usr/local/php/sbin/php-fpm
# 如果需要绑定低端口(<1024),只授予net_bind_service
setcap cap_net_bind_service=+ep /usr/local/php/sbin/php-fpm
# 使用systemd限制
# /etc/systemd/system/php-fpm.service.d/override.conf
[Service]
# 禁止访问/home
ProtectHome=true
# 禁止访问/usr、/boot、/etc(只读)
ProtectSystem=full
# 私有/tmp
PrivateTmp=true
# 私有设备节点
PrivateDevices=true
# 禁止设置setuid/setgid
NoNewPrivileges=true
# 能力限制
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# 系统调用过滤
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
7.3 chroot环境
php
<?php
/**
* chroot环境配置
*/
// 注意:PHP的chroot需要root权限,通常由FPM配置
// FPM池配置:chroot = /var/www/site1
// 创建chroot环境
class ChrootBuilder {
private $chrootPath;
public function __construct(string $path) {
$this->chrootPath = $path;
}
/**
* 创建最小chroot环境
*/
public function build(): void {
// 创建目录结构
$dirs = [
'etc',
'tmp',
'var/tmp',
'lib',
'lib64',
'usr/lib',
'usr/lib64',
];
foreach ($dirs as $dir) {
$fullPath = $this->chrootPath . '/' . $dir;
if (!is_dir($fullPath)) {
mkdir($fullPath, 0755, true);
}
}
// 复制必要的库文件
$this->copyLibraries();
// 创建最小/etc/passwd
$this->createPasswd();
// 设置权限
chmod($this->chrootPath . '/tmp', 1777);
}
private function copyLibraries(): void {
// 复制PHP依赖的库
$libs = [
'/lib/x86_64-linux-gnu/libc.so.6',
'/lib/x86_64-linux-gnu/libdl.so.2',
'/lib/x86_64-linux-gnu/libm.so.6',
'/lib/x86_64-linux-gnu/libpthread.so.0',
'/lib/x86_64-linux-gnu/libresolv.so.2',
'/lib64/ld-linux-x86-64.so.2',
];
foreach ($libs as $lib) {
if (file_exists($lib)) {
copy($lib, $this->chrootPath . $lib);
}
}
}
private function createPasswd(): void {
$passwd = "www-data:x:33:33:www-data:/var/www:/bin/false\n";
file_put_contents($this->chrootPath . '/etc/passwd', $passwd);
}
}
?>
8. 容器化环境安全
8.1 Docker安全配置
dockerfile
# =====================================================
# 安全的PHP Docker镜像
# =====================================================
FROM php:8.1-fpm-alpine
# 安装依赖
RUN apk add --no-cache \
libpng-dev \
libzip-dev \
&& docker-php-ext-install \
pdo_mysql \
gd \
zip \
opcache
# 复制自定义php.ini
COPY php-production.ini /usr/local/etc/php/php.ini
COPY php-fpm.conf /usr/local/etc/php-fpm.d/www.conf
# 创建非root用户
RUN addgroup -g 1000 -S www && \
adduser -u 1000 -S www -G www
# 创建必要的目录
RUN mkdir -p /var/www/html /var/log/php /var/www/sessions && \
chown -R www:www /var/www /var/log/php
# 设置工作目录
WORKDIR /var/www/html
# 切换到非root用户
USER www
# 暴露端口
EXPOSE 9000
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD php-fpm -t || exit 1
CMD ["php-fpm"]
yaml
# docker-compose.yml安全配置
version: '3.8'
services:
php:
build:
context: .
dockerfile: Dockerfile
read_only: true # 只读根文件系统
tmpfs:
- /tmp:noexec,nosuid,size=100m
- /var/www/tmp:noexec,nosuid,size=100m
volumes:
- ./src:/var/www/html:ro # 代码只读
- php-sessions:/var/www/sessions
- php-logs:/var/log/php
environment:
- PHP_MEMORY_LIMIT=128M
- PHP_MAX_EXECUTION_TIME=30
ulimits:
nproc: 50
nofile:
soft: 1024
hard: 2048
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
networks:
- backend
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./src:/var/www/html:ro
depends_on:
- php
networks:
- frontend
- backend
volumes:
php-sessions:
php-logs:
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 不连接到外部网络
8.2 Kubernetes安全配置
yaml
# php-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-app
spec:
replicas: 3
selector:
matchLabels:
app: php
template:
metadata:
labels:
app: php
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: php
image: myapp/php:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: sessions
mountPath: /var/www/sessions
- name: logs
mountPath: /var/log/php
- name: code
mountPath: /var/www/html
readOnly: true
livenessProbe:
exec:
command:
- php-fpm
- -t
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: tmp
emptyDir:
sizeLimit: 100Mi
- name: sessions
emptyDir:
sizeLimit: 100Mi
- name: logs
emptyDir:
sizeLimit: 50Mi
- name: code
configMap:
name: php-code
---
apiVersion: v1
kind: NetworkPolicy
metadata:
name: php-network-policy
spec:
podSelector:
matchLabels:
app: php
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: nginx
ports:
- protocol: TCP
port: 9000
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 3306
8.3 容器安全扫描
php
<?php
/**
* 容器环境安全检测
*/
class ContainerSecurityCheck {
/**
* 运行安全检查
*/
public function runChecks(): array {
$results = [];
$results['root_user'] = $this->checkNotRunningAsRoot();
$results['read_only_fs'] = $this->checkReadOnlyFilesystem();
$results['capabilities'] = $this->checkCapabilities();
$results['writable_dirs'] = $this->checkWritableDirectories();
$results['env_vars'] = $this->checkSensitiveEnvVars();
return $results;
}
/**
* 检查是否以root运行
*/
private function checkNotRunningAsRoot(): array {
$uid = posix_geteuid();
$user = posix_getpwuid($uid);
return [
'pass' => $uid !== 0,
'message' => $uid === 0 ? 'Running as root' : "Running as {$user['name']}",
'uid' => $uid
];
}
/**
* 检查文件系统是否只读
*/
private function checkReadOnlyFilesystem(): array {
$testFile = '/.write_test_' . uniqid();
$canWrite = @file_put_contents($testFile, 'test') !== false;
if ($canWrite) {
unlink($testFile);
}
return [
'pass' => !$canWrite,
'message' => $canWrite ? 'Filesystem is writable' : 'Filesystem is read-only'
];
}
/**
* 检查Linux capabilities
*/
private function checkCapabilities(): array {
$capsFile = '/proc/self/status';
if (!file_exists($capsFile)) {
return ['pass' => null, 'message' => 'Cannot check capabilities'];
}
$status = file_get_contents($capsFile);
preg_match('/CapEff:\s*(\w+)/', $status, $matches);
$caps = $matches[1] ?? 'unknown';
// 检查是否有危险的能力
$dangerousCaps = ['cap_sys_admin', 'cap_net_admin', 'cap_sys_ptrace'];
return [
'pass' => $caps === '0000000000000000',
'message' => "Effective capabilities: $caps",
'caps' => $caps
];
}
/**
* 检查可写目录
*/
private function checkWritableDirectories(): array {
$dirs = ['/tmp', '/var/tmp', '/var/www/sessions'];
$writable = [];
foreach ($dirs as $dir) {
if (is_dir($dir) && is_writable($dir)) {
$writable[] = $dir;
}
}
return [
'pass' => true, // 有可写目录是正常的
'message' => 'Writable directories: ' . implode(', ', $writable),
'dirs' => $writable
];
}
/**
* 检查敏感环境变量
*/
private function checkSensitiveEnvVars(): array {
$sensitivePatterns = ['password', 'secret', 'key', 'token', 'credential'];
$sensitiveVars = [];
foreach ($_ENV as $key => $value) {
foreach ($sensitivePatterns as $pattern) {
if (stripos($key, $pattern) !== false) {
$sensitiveVars[] = $key;
}
}
}
return [
'pass' => empty($sensitiveVars),
'message' => empty($sensitiveVars)
? 'No sensitive env vars exposed'
: 'Potentially sensitive env vars: ' . implode(', ', $sensitiveVars),
'vars' => $sensitiveVars
];
}
/**
* 生成报告
*/
public function generateReport(): string {
$checks = $this->runChecks();
$report = "Container Security Check\n";
$report .= str_repeat('=', 40) . "\n\n";
foreach ($checks as $check => $result) {
$icon = $result['pass'] === true ? '✓' : ($result['pass'] === false ? '✗' : '?');
$report .= "$icon $check: {$result['message']}\n";
}
return $report;
}
}
// 在容器启动时运行
if (getenv('RUN_SECURITY_CHECK')) {
$check = new ContainerSecurityCheck();
echo $check->generateReport();
}
?>
9. 总结与延伸
9.1 关键要点回顾
-
SAPI选择影响安全边界
- CLI拥有最高权限,需谨慎使用
- FPM提供最佳的多租户隔离
- Apache模块简单但不适合共享环境
-
权限最小化原则
- Web应用永远不要以root运行
- 每个站点使用独立的系统用户
- 利用Linux capabilities限制权限
-
资源隔离
- 使用open_basedir限制文件访问
- 通过FPM进程池实现资源隔离
- 容器环境使用只读文件系统
-
监控与审计
- 定期检查SAPI配置变更
- 监控FPM进程状态
- 审计文件系统权限
-
容器安全
- 使用非root用户运行
- 限制capabilities
- 应用网络隔离策略
9.2 常见问题速答
Q1: 如何选择合适的SAPI?
| 场景 | 推荐SAPI | 原因 |
|---|---|---|
| 生产Web应用 | FPM | 性能、隔离、监控能力 |
| 共享主机 | FPM (多池) | 用户隔离 |
| 命令行脚本 | CLI | 唯一选择 |
| 简单内部工具 | Apache模块 | 部署简单 |
| 嵌入式应用 | Embed | 宿主控制 |
Q2: FPM和Apache模块哪个更安全?
FPM更安全,原因:
- 进程隔离:单个请求崩溃不影响其他请求
- 用户隔离:每个池可以运行不同用户
- 资源限制:更细粒度的资源控制
- 监控能力:内置状态页和慢日志
Q3: 如何检测当前运行的SAPI?
php
<?php
// 方法1: 使用常量
echo PHP_SAPI; // 如 'fpm-fcgi', 'cli', 'apache2handler'
// 方法2: 使用函数
echo php_sapi_name(); // 同上
// 方法3: 检测特定SAPI
if (PHP_SAPI === 'cli') {
echo "Running in CLI\n";
}
if (strpos(PHP_SAPI, 'apache') !== false) {
echo "Running as Apache module\n";
}
?>
Q4: 容器中的PHP应该使用什么用户?
dockerfile
# 创建专用用户(避免使用www-data,太常见)
RUN addgroup -g 1000 -S appgroup && \
adduser -u 1000 -S appuser -G appgroup
USER appuser