0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テスト環境が検索に出る・丸見えを防ぐ5つの設定(nginx/Apache/WordPress・コピペOK)

0
Posted at
  • 誰向け: 制作会社・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. まず 設定1・設定2 を入れる(恒久対応)
  2. Google Search Console でテスト環境のドメインを所有確認し、「削除」ツールで一時削除をリクエストする
  3. 一時削除は 約 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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?