NoteStack

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 Report-Only を安全に運用開始する(Nginx + PHP + SELinux)

「CSPを入れたいけど、いきなり厳しくするとサイトが壊れそう」——この不安は正しいです。CSPは強力な反面、いきなり強制(enforce)すると広告・解析・外部JSなどが止まりやすいため、まずはReport-Only(観測のみ)から始めるのが定石です。

このページでは、Report-Onlyで“壊さず観測”を開始するために必要な「受け口の作成 → Nginx側で防御 → ログ保存 → 受信確認 → SELinuxの詰まり解消」までを、初心者向けに段階化して説明します。個人情報になりやすい部分(ドメイン名・パス・ユーザー名など)は example.com や一般的な例に置き換えて説明します。

このページでできるようになること
このページでは“まだやらないこと”

先に結論:最短で迷わない手順(全体像)

ステップ1:受信口を作る(URLと処理を分離)

まずは https://example.com/csp-report で受け取れるようにします。ポイントは、サイト本体の location /location ~ \.php$ に混ぜず、受信口専用の location を作ることです。

ステップ2:Nginx側で防御(壊れる前に入口で止める)
  • POST以外は 405
  • ボディサイズ制限(例:100k
  • レート制限(例:1分30回)
ステップ3:PHPで受けてログ保存(まずは一行ログでOK)

最初は「受け取ったことが分かる」だけで十分です。JSONを綺麗に解析するのは後でOKです。

ステップ4:Report-Onlyヘッダーを付けて観測開始

レスポンスに Content-Security-Policy-Report-Only が付いていることをブラウザのDevToolsで確認します。

ステップ5:ログが出ないときはSELinuxを疑う

AlmaLinux/RHEL系で SELinux が Enforcing の場合、ファイル権限がOKでも書けないことがあります。disableはせずにlogs だけに書き込み許可ラベルを付けます。

まず理解:Report-Only は「観測するだけ」

Report-Only の役割

Report-Only は、CSP違反が起きてもブロックしません。代わりに「こういう違反が起きたよ」というレポート(通知)を送ります。

なぜ最初はReport-Onlyが安全?

広告・解析・外部CDN・埋め込みなどは、CSPを強制すると止まりやすいです。Report-Onlyなら、まず“何が必要か”を観測してから許可リストを固められます

よくある誤解

「Report-Onlyを付けたらセキュリティが上がる?」→ 基本的に上がりません(ブロックしないため)。ただし、本番CSPへ移行するための必須準備として価値が高いです。

ステップ1:CSPレポート受信URL(受け口)を作る

ここがいちばん大事です。受信口は「サイト本体」と分けます。例として、次のような構成にします。

/var/www/example.com/
 ├ public/        ← 表示用(ドキュメントルート)
 ├ csp/           ← CSP受信専用(publicの外)
 │   └ csp-report.php
 └ logs/          ← 受信ログ保存(publicの外)

ポイントは2つです。

ステップ2:Nginxで受信口を“狙い撃ち”して防御する

2-1) レート制限ゾーンは http ブロックに書く

limit_req_zoneserver { ... } の外(http { ... } 内)に書きます。

# /etc/nginx/nginx.conf の http { ... } 内に追加
limit_req_zone $binary_remote_addr zone=csp_zone:10m rate=30r/m;

2-2) /csp-report 専用 location を追加(ここが本体)

既存の 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;
}

この時点で受信口は、壊れにくい“入口防御”ができています。

2-3) 反映(毎回この順)

sudo nginx -t
sudo systemctl reload nginx

ステップ3:PHPは「受け取って返すだけ」から始める

最初のゴールは、受信できていることが分かることです。余計なことはしません。ログは 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保存など)にも移行しやすいです。

ステップ4:Report-Onlyヘッダーを付けて観測開始

受信口ができたら、次にページ側へ Report-Only を付けます。まずは最小でOKです。

add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report;" always;

ブラウザでの確認(どのURLを開く?)

Report-Only は「そのサーバーが返すページ」に付くので、普段のページ(例:トップページ、アプリのURL)を開けばOKです。

確認手順(Chrome/Edge)
  1. 対象ページを開く(例:https://example.com/
  2. DevTools → Network を開く
  3. ページをリロード
  4. Type が document のリクエストを選ぶ
  5. Response Headers に 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 で返り、ログが増えます。

ステップ5:ログが作られないとき(SELinux Enforcing の正攻法)

RHEL系(AlmaLinux/Rocky 等)で SELinux が Enforcing の場合、chmod/chown が正しくても PHP-FPMから書けないことがあります。これはセキュリティ機構が働いているだけなので、disableはしません。

5-1) まずは状況確認

# logs のSELinuxラベルを見る
ls -ldZ /var/www/example.com/logs

# 拒否ログ(avc: denied)を見る
sudo grep -i denied /var/log/audit/audit.log | tail -n 30

5-2) logs だけに「書いてよい」ラベルを付ける(永続)

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

5-3) 最終確認

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運用開始の“受信基盤”は完成です。

チェックリスト(この順でやれば迷わない)

  1. 受信口のURLを決める:/csp-report
  2. Nginx:limit_req_zonehttp {} に追加
  3. Nginx:location = /csp-report を追加(POST/サイズ/レート制限)
  4. PHP:受信して204を返し、logs/csp-received.log に一行書く
  5. Report-Onlyヘッダーを追加
  6. DevToolsでヘッダー確認
  7. curl で受信確認(必要なら)
  8. ログが出ない→ audit.log でSELinux拒否確認→ httpd_sys_rw_content_t を logs のみに付与

よくある質問(FAQ)

Q. Report-Only は「セキュリティ強化」になりますか?
A. 基本的にブロックはしないので、単体では強化になりません。ただし、本番CSP(強制)へ安全に移行するための必須ステップです。
Q. 429 が出ない(503 が出る)
A. limit_req のデフォルトは 503 です。運用上の意味を明確にしたいなら limit_req_status 429; を入れます。
Q. chmod/chown はOKなのにログが作れません
A. SELinux(Enforcing) が原因のことがあります。audit.logavc: denied が出ていれば確定です。disableはせず、logs だけに httpd_sys_rw_content_t を付けるのが正攻法です。
Q. 受信口のPHPは public に置いてはいけない?
A. “置ける”こともありますが、公開領域から分離した方が安全です。Nginxの fastcgi_param SCRIPT_FILENAME で public 外の実ファイルへ直接渡す構成が事故りにくいです。