This page is a beginner-friendly guide to start CSP in Report-Only mode safely: create a dedicated report endpoint, add rate limiting and request constraints in Nginx, log reports with PHP, and fix SELinux write denials the right way.
「CSPを入れたいけど、いきなり厳しくするとサイトが壊れそう」——この不安は正しいです。CSPは強力な反面、いきなり強制(enforce)すると広告・解析・外部JSなどが止まりやすいため、まずはReport-Only(観測のみ)から始めるのが定石です。
このページでは、Report-Onlyで“壊さず観測”を開始するために必要な「受け口の作成 → Nginx側で防御 → ログ保存 → 受信確認 → SELinuxの詰まり解消」までを、初心者向けに段階化して説明します。個人情報になりやすい部分(ドメイン名・パス・ユーザー名など)は example.com や一般的な例に置き換えて説明します。
まずは https://example.com/csp-report で受け取れるようにします。ポイントは、サイト本体の location / や location ~ \.php$ に混ぜず、受信口専用の location を作ることです。
405100k)最初は「受け取ったことが分かる」だけで十分です。JSONを綺麗に解析するのは後でOKです。
レスポンスに Content-Security-Policy-Report-Only が付いていることをブラウザのDevToolsで確認します。
AlmaLinux/RHEL系で SELinux が Enforcing の場合、ファイル権限がOKでも書けないことがあります。disableはせずに、logs だけに書き込み許可ラベルを付けます。
Report-Only は、CSP違反が起きてもブロックしません。代わりに「こういう違反が起きたよ」というレポート(通知)を送ります。
広告・解析・外部CDN・埋め込みなどは、CSPを強制すると止まりやすいです。Report-Onlyなら、まず“何が必要か”を観測してから許可リストを固められます。
「Report-Onlyを付けたらセキュリティが上がる?」→ 基本的に上がりません(ブロックしないため)。ただし、本番CSPへ移行するための必須準備として価値が高いです。
ここがいちばん大事です。受信口は「サイト本体」と分けます。例として、次のような構成にします。
/var/www/example.com/
├ public/ ← 表示用(ドキュメントルート)
├ csp/ ← CSP受信専用(publicの外)
│ └ csp-report.php
└ logs/ ← 受信ログ保存(publicの外)
ポイントは2つです。
limit_req_zone は server { ... } の外(http { ... } 内)に書きます。
# /etc/nginx/nginx.conf の http { ... } 内に追加
limit_req_zone $binary_remote_addr zone=csp_zone:10m rate=30r/m;
既存の location / や location ~ \.php$ には書きません。必ず「完全一致」の受信口を新規で作ります。
# HTTPS側の server { ... } 内
location = /csp-report {
# レート制限(制限に当たったら 429 にする)
limit_req zone=csp_zone burst=30 nodelay;
limit_req_status 429;
# POST 以外は拒否
if ($request_method != POST) { return 405; }
# サイズ制限(入口で落とす)
client_max_body_size 100k;
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm/www.sock;
# public配下ではなく、cspディレクトリの実ファイルへ
fastcgi_param SCRIPT_FILENAME /var/www/example.com/csp/csp-report.php;
}
この時点で受信口は、壊れにくい“入口防御”ができています。
sudo nginx -t
sudo systemctl reload nginx
最初のゴールは、受信できていることが分かることです。余計なことはしません。ログは logs/ にだけ書きます。
<?php
declare(strict_types=1);
// 受信口は“静かに204を返す”のが基本
http_response_code(204);
// POST以外は想定しない(Nginx側でも弾く)
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
// JSONを読む(失敗しても落とさない)
$raw = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true);
// 保存先:/var/www/example.com/logs/csp-received.log
$baseDir = dirname(__DIR__);
$logDir = $baseDir . '/logs';
$logFile = $logDir . '/csp-received.log';
// まずは“受信した”を残すだけ(書けないなら黙ってスキップ)
if (is_dir($logDir) && is_writable($logDir)) {
file_put_contents($logFile, date('c') . " received\n", FILE_APPEND | LOCK_EX);
}
?>
この設計にすると、受信が増えてもページ表示が壊れにくく、次の段階(SQLite保存など)にも移行しやすいです。
受信口ができたら、次にページ側へ Report-Only を付けます。まずは最小でOKです。
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report;" always;
Report-Only は「そのサーバーが返すページ」に付くので、普段のページ(例:トップページ、アプリのURL)を開けばOKです。
https://example.com/)content-security-policy-report-only があることを確認ブラウザの送信条件は環境差があるため、確実に受信テストをしたい場合は curl で送ります。
curl -i -X POST https://example.com/csp-report \
-H "Content-Type: application/json" \
--data '{"csp-report":{"document-uri":"https://example.com/","violated-directive":"script-src","blocked-uri":"https://example.com/test.js"}}'
成功の目安:HTTPが 204 で返り、ログが増えます。
RHEL系(AlmaLinux/Rocky 等)で SELinux が Enforcing の場合、chmod/chown が正しくても PHP-FPMから書けないことがあります。これはセキュリティ機構が働いているだけなので、disableはしません。
# logs のSELinuxラベルを見る
ls -ldZ /var/www/example.com/logs
# 拒否ログ(avc: denied)を見る
sudo grep -i denied /var/log/audit/audit.log | tail -n 30
logs 以外は触らないのが安全です。
# semanage が使えることを確認(ヘルプが出ればOK)
semanage --help
# logs 配下に httpd の書き込み許可ラベルを永続登録
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/example.com/logs(/.*)?"
# 実ファイルへ反映
sudo restorecon -Rv /var/www/example.com/logs
ls -ldZ /var/www/example.com/logs
ls -l /var/www/example.com/logs
sudo tail -n 30 /var/www/example.com/logs/csp-received.log
ここまで通れば、Report-Only運用開始の“受信基盤”は完成です。
/csp-reportlimit_req_zone を http {} に追加location = /csp-report を追加(POST/サイズ/レート制限)logs/csp-received.log に一行書くcurl で受信確認(必要なら)audit.log でSELinux拒否確認→ httpd_sys_rw_content_t を logs のみに付与limit_req のデフォルトは 503 です。運用上の意味を明確にしたいなら limit_req_status 429; を入れます。chmod/chown はOKなのにログが作れませんaudit.log に avc: denied が出ていれば確定です。disableはせず、logs だけに httpd_sys_rw_content_t を付けるのが正攻法です。fastcgi_param SCRIPT_FILENAME で public 外の実ファイルへ直接渡す構成が事故りにくいです。