-
誰向け: 制作会社・Web 担当者で、
stg.dev.test.などのテスト環境(ステージング)を公開サーバー上に置いている方 - 何が解決するか: テスト環境が「検索結果に出る」「URL を知っている誰でも見られる」事故を、サーバー設定とWordPress 設定の 5 か所で塞ぎます
- 前提: nginx / Apache(2.4)/ WordPress 5.5 以降。設定はすべてコピペで動く完全形です
なぜテスト環境は漏れるのか
テスト環境の露出は、攻撃というより 「誰も見ないはず」という思い込み から起きる事故です。よくある経路は次の 3 つです。
| 経路 | 何が起きるか |
|---|---|
| 検索エンジン | 本番と同じ中身のページがインデックスされ、重複コンテンツ扱いや「作りかけのページ」が検索結果に出る |
| 証明書の公開ログ | HTTPS 化のために証明書を取った時点で、ホスト名(stg.example.jp など)が公開ログに載る |
| 本番 DB のコピー | ステージングに本番の会員・問い合わせデータをそのまま入れ、認証なしで管理画面やフォームが見える |
2 つ目は見落とされがちです。ブラウザが信頼する証明書は Certificate Transparency(CT)ログ(発行された証明書を記録する公開台帳)への登録が必須で、Chrome・Safari・Firefox はいずれも CT を要求しています。「推測されにくいサブドメイン名なら見つからない」は成立しません。
robots.txt も守りになりません。Google 公式ドキュメントは robots.txt を「Google にページを載せないための仕組みではない」と明記し、Disallow したページでも 他サイトからリンクされていればインデックスされ得る としています。見られたくないなら、名前や robots.txt ではなく アクセス制御 で守ります。
設定1: Basic 認証 + IP 許可で「そもそも見せない」
最優先はアクセス制御です。Google のヘルプでも、コンテンツを検索結果から恒久的に外す方法として「パスワードでアクセスを制限する」ことが挙げられています。認証で 401 を返していればクローラーは中身を取得できない ので、検索対策としても最も確実です。
パスワードファイルを作る
Apache 付属の htpasswd で作成します。既定のハッシュ方式は MD5 なので、-B で bcrypt を指定します。
# 初回だけ -c(既存ファイルがあると中身を消して作り直すので 2 人目以降は付けない)
sudo htpasswd -c -B /etc/nginx/.htpasswd-stg staff01
# 2 人目以降は -c なしで追記
sudo htpasswd -B /etc/nginx/.htpasswd-stg staff02
# Web サーバーのユーザーだけが読めるようにする(Debian/Ubuntu の例)
sudo chown root:www-data /etc/nginx/.htpasswd-stg
sudo chmod 640 /etc/nginx/.htpasswd-stg
パスワードファイルは ドキュメントルートの外 に置きます。ドキュメントルート内に置くと、それ自体がダウンロードされる事故の種になります。
nginx: 社内 IP は素通し、それ以外はパスワード
satisfy any; は「IP 許可 または Basic 認証のどちらかが通ればよい」という指定です(既定は satisfy all;)。
server {
listen 443 ssl;
http2 on;
server_name stg.example.jp;
ssl_certificate /etc/letsencrypt/live/stg.example.jp/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/stg.example.jp/privkey.pem;
root /var/www/stg/public;
# --- ここからテスト環境の入口制御 ---
satisfy any;
allow 203.0.113.10; # 事務所の固定 IP(自社の値に置き換え)
allow 198.51.100.0/28; # VPN の出口(自社の値に置き換え)
deny all;
auth_basic "Staging";
auth_basic_user_file /etc/nginx/.htpasswd-stg;
# 証明書の自動更新(HTTP-01)を認証で止めない
location ^~ /.well-known/acme-challenge/ {
auth_basic off;
allow all;
}
# 検索エンジン向けの保険(設定2)
add_header X-Robots-Tag "noindex, nofollow" always;
location / {
try_files $uri $uri/ /index.php?$args;
}
}
ポート 80 側の server ブロックは HTTPS への 301 転送だけにしておきます。http2 on; は nginx 1.25.1 以降の書き方です(古い版は listen 443 ssl http2;)。
Apache 2.4: 同じことを RequireAny で
Apache 2.4 では <RequireAny> の中に並べた条件のどれかを満たせば許可されます。
<VirtualHost *:443>
ServerName stg.example.jp
DocumentRoot /var/www/stg/public
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/stg.example.jp/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/stg.example.jp/privkey.pem
<Directory /var/www/stg/public>
AllowOverride All
AuthType Basic
AuthName "Staging"
AuthBasicProvider file
AuthUserFile /etc/apache2/.htpasswd-stg
<RequireAny>
Require ip 203.0.113.10
Require ip 198.51.100.0/28
Require valid-user
</RequireAny>
</Directory>
# 証明書の自動更新(HTTP-01)を認証で止めない
<Location "/.well-known/acme-challenge/">
Require all granted
</Location>
# 検索エンジン向けの保険(設定2)
Header always set X-Robots-Tag "noindex, nofollow"
</VirtualHost>
Header を使うには mod_headers が必要です(sudo a2enmod headers)。
注意: CDN / プロキシ配下では IP 許可が効かない・効きすぎる
Cloudflare やロードバランサー配下では、サーバーから見た接続元が プロキシの IP になります。このまま allow を書くと、社内 IP が許可されない、あるいはプロキシの IP を許可したせいで全員が素通りする、のどちらかになります。配下にある場合は real IP の設定(nginx の realip / Apache の mod_remoteip)を先に済ませるか、テスト環境だけはプロキシを通さず Basic 認証のみ で守る方が安全です。
設定2: X-Robots-Tag で「万一見えても載せない」
認証を入れたうえで、保険としてレスポンスヘッダ X-Robots-Tag: noindex を全レスポンスに付けます(上の設定例に含めています)。
-
<meta name="robots">と違い、PDF・画像・JSON にも 効きます - nginx では
alwaysを付けないと 404 や 500 の応答に付きません
ここで絶対にやってはいけないのが robots.txt で Disallow: / と noindex の併用 です。Google のドキュメントには「robots.txt でブロックされているとクローラーは noindex を見ることができず、ページが検索結果に表示される可能性がある」とあります。テスト環境の robots.txt は Disallow しない(もしくは置かない)のが正解です。
確認はコマンド 1 本で済みます。
# 認証なし: 401 が返り、X-Robots-Tag が付いていれば OK
curl -sI https://stg.example.jp/ | grep -iE '^HTTP/|^x-robots-tag'
# 認証あり: 200 と X-Robots-Tag
curl -sI -u staff01 https://stg.example.jp/ | grep -iE '^HTTP/|^x-robots-tag'
設定3: WordPress に「ここは本番ではない」と教える
WordPress 5.5 以降は、wp-config.php の定数 WP_ENVIRONMENT_TYPE で環境の種類を宣言できます。値は local / development / staging / production の 4 つで、未設定なら production 扱い です。
// wp-config.php(「編集が必要なのはここまで」の行より上に書く)
define( 'WP_ENVIRONMENT_TYPE', 'staging' );
この値を見て、mu-plugin(wp-content/mu-plugins/ に置くと自動で有効になるプラグイン)で 本番以外のときだけ 次の 3 つを強制します。本番の wp-config.php には書かない(=既定の production)ので、同じ mu-plugin を本番に置いても何も起きません。
<?php
/**
* Plugin Name: Staging Guard
* Description: 本番以外の環境で、検索エンジン除外・メール送信停止を強制する
*/
if ( 'production' === wp_get_environment_type() ) {
return; // 本番では何もしない
}
// 1. 「検索エンジンがサイトをインデックスしないようにする」を常にオン扱い
add_filter( 'pre_option_blog_public', '__return_zero' );
// 2. robots メタに noindex を出す(WordPress 5.7 以降の wp_robots API / 1 の結果でも出るが念のため明示)
add_filter( 'wp_robots', 'wp_robots_no_robots' );
// 3. 本番データのコピーから実在の顧客へメールが飛ぶ事故を防ぐ
add_filter( 'pre_wp_mail', function ( $return, $atts ) {
error_log( '[staging] mail suppressed: ' . wp_json_encode( $atts['to'] ) );
return true; // 送信したことにして実際には送らない
}, 10, 2 );
管理画面の「検索エンジンでの表示」の値 blog_public は DB に入っているため、本番 DB をコピーするたびに「インデックスを許可」に戻ります。チェックボックスに頼らずコードで固定するのがポイントです。確認は wp-cli で行います。
cd /var/www/stg/public
wp eval 'echo wp_get_environment_type() . PHP_EOL;' # staging と出れば OK
wp option get blog_public # 0 と出れば OK(フィルタ適用後の値)
pre_wp_mail は WordPress 5.7 以降のフィルタです。フォームのテストでメールを確認したい場合は、止めずに 宛先をテスト用アドレスへ書き換える 形にします。
設定4: 本番データを持ち込まない
テスト環境は本番より 更新が遅れがちで監視も薄い のが普通です。そこに本番の個人情報を丸ごと置くと、一番弱い入口になります。
- 会員・注文・問い合わせのデータは、コピー後に メールアドレスや氏名をダミーに置き換える
- 決済や外部 API のキーは テスト用(サンドボックス)に差し替える(本番キーのままだとテスト注文が本物の決済として処理される)
- 定期処理(WP-Cron)が本番宛ての外部連携を叩かないか確認する
WordPress ならコピー直後に次を実行します(接頭辞 wp_ とドメインは自社の値に置き換え)。
cd /var/www/stg/public
# 本番 URL をテスト環境の URL に置き換える(先に --dry-run で件数確認)
wp search-replace 'https://www.example.jp' 'https://stg.example.jp' --all-tables-with-prefix --dry-run
wp search-replace 'https://www.example.jp' 'https://stg.example.jp' --all-tables-with-prefix
# 管理者以外のユーザーのメールアドレスをダミーに置き換える
wp db query "UPDATE wp_users SET user_email = CONCAT('user', ID, '@example.invalid') WHERE ID NOT IN (SELECT user_id FROM wp_usermeta WHERE meta_key = 'wp_capabilities' AND meta_value LIKE '%administrator%');"
# 予定されている定期処理を一覧で確認
wp cron event list
.invalid は実在しないことが保証されたトップレベルドメインなので、万一送信処理が動いても誰にも届きません。
設定5: すでに載ってしまったときと、使い終わったとき
すでに検索結果に出ている場合
- まず 設定1・設定2 を入れる(恒久対応)
- Google Search Console でテスト環境のドメインを所有確認し、「削除」ツールで一時削除をリクエストする
- 一時削除は 約 6 か月で切れる ので、それまでに 1 の状態(認証で 401 / noindex / 404・410 のいずれか)になっていることを確認する
Google のヘルプでは、恒久的に外すには「404 か 410 を返す」「パスワードで保護する」「noindex を指定する」のいずれかが必要とされています。削除ツールだけで終わらせると、半年後に再び表示されます。自社ドメインの証明書ログ(crt.sh で %.example.jp を検索)に出てくるホスト名は、今も使っているか・認証がかかっているかを 1 つずつ確認しておきましょう。
使い終わったテスト環境
リニューアル公開後にテスト環境が放置され、古い WordPress やプラグインのまま何年も動き続ける のが最も多い事故パターンです。
- 公開日に「テスト環境を止める日」も決めて手順書に書く
- 外部のホスティングサービス上に作ったテスト環境は、サービス側を解約する前に DNS レコードを消す(行き先の無くなった CNAME が残るとサブドメイン乗っ取りの原因になるため)
- 自社サーバー上なら、ファイル・DB を削除したうえで設定1 の server / VirtualHost ごと外し、DNS レコードも消す
- 残す場合も、本番と同じ頻度で WordPress 本体とプラグインを更新する
まとめ
| # | 設定 | 防げること |
|---|---|---|
| 1 | Basic 認証 + IP 許可(satisfy any / RequireAny) |
誰でも見られる状態・インデックスの両方 |
| 2 |
X-Robots-Tag: noindex(robots.txt で Disallow しない) |
認証を外した期間のインデックス |
| 3 |
WP_ENVIRONMENT_TYPE + mu-plugin |
DB 同期で検索表示が戻る事故・誤メール送信 |
| 4 | 本番データの匿名化・キー差し替え | テスト環境経由の個人情報漏えい・誤決済 |
| 5 | 削除ツール + 恒久対応 / 撤去日の決定 | 載ってしまった後の後始末・放置環境 |
「推測されにくい URL」と「robots.txt」は、どちらもテスト環境を守る手段になりません。認証で見せない → ヘッダで載せない → WordPress 側で戻らないようにする の 3 段を、テスト環境を作るときの定型手順にしておくのがおすすめです。
関連記事
本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp