ステージング環境や社内テスト用の WordPress サイトに、念のため HTTP Basic 認証をかけているケースは結構あります。本番公開前のサイト・社員しか触らないテストインスタンス・特定 IP からだけ参照したい環境 — どれもログイン画面より外側に「もう 1 段の壁」を置く運用です。
保守ツールの目線でこれを扱おうとすると、独特の「半分動く・半分動かない」非対称な状態に陥ります。SSH/WP-CLI 経由のメンテナンス本体は普通に通る。一方で、HTTP ベースの確認系(ビジュアルチェック・サムネ取得・ブラウザ補完更新)が全部 401 で死ぬ。今回はこの非対称を解いた話を整理しておきます。
何が起きていたか — 2 系統が同時に詰まる
Basic 認証で保護されたサイトに対して、保守ツールが触る経路は実は 2 系統あります。
- Playwright 経路: ビジュアルチェック・サムネ取得・SSH 不可環境でのブラウザ補完更新。
browser.new_context()→ ページ移動 → スクリーンショット - urllib 経路: HTTP ステータスチェック(更新前後の 200/5xx/4xx 監視・ロールバック判定)
両方とも、認証情報なしで Basic 認証保護サイトにアクセスすると 401 Unauthorized が返ってくる。
Playwright 側の症状は分かりやすくて、撮影されるスクリーンショットがブラウザ標準の「認証が必要です」ダイアログ画像になります。サムネ一覧に黒っぽい認証画面のサムネが並んで「これ本当に動いてる?」感が出ます。
urllib 側はもっと悪質で、ロールバック判定が壊れる。更新前ベースラインで 401・更新後も 401 だと、「変化なし = 健全」と誤判定されて、本来異常が起きていてもロールバックが発動しない経路があります。
設計 — 認証情報の取り出しを 1 つのヘルパーに集約
両経路に同じ認証情報を渡す必要がある時、各箇所で個別に site dict から取り出すと、書き漏らしや形式の不一致が起こりやすい。最初に core/basic_auth_utils.py という小さなヘルパーモジュールを 1 つ作って、そこに全形式の取り出し関数を集めることにしました。
# core/basic_auth_utils.py
def get_basic_auth_tuple(site):
"""site から (user, password) のタプルを返す。未設定なら None。"""
if not isinstance(site, dict):
return None
user = (site.get('basic_auth_user') or '').strip()
pw = site.get('basic_auth_password') or ''
if not user:
return None # user が空なら認証なしと判定
return (user, pw)
def get_playwright_http_credentials(site):
"""Playwright の new_context(http_credentials=...) 用 dict。"""
auth = get_basic_auth_tuple(site)
if auth is None:
return None
return {'username': auth[0], 'password': auth[1]}
def get_basic_auth_header(site):
"""urllib などの HTTP ヘッダー用 {'Authorization': 'Basic <base64>'}。"""
auth = get_basic_auth_tuple(site)
if auth is None:
return {}
raw = f"{auth[0]}:{auth[1]}".encode('utf-8')
encoded = base64.b64encode(raw).decode('ascii')
return {'Authorization': f'Basic {encoded}'}
ポイントは 同じ get_basic_auth_tuple() を起点に、Playwright 形式・urllib 形式の両方を派生させていること。2 系統で (user, password) の解釈がズレる事故を構造的に防げます。
後方互換 — 既存サイトには新フィールドが無い
ここで地味に大事だったのが、既存のサイト設定 JSON には basic_auth_user / basic_auth_password キーが存在しないこと。何も考えずに site['basic_auth_user'] のように書くと、既存サイトを開いた瞬間に KeyError でクラッシュする経路ができてしまいます。
採用したのは site.get('basic_auth_user') or '' の空文字フォールバックパターン。「キー無し / 空文字 / None」のいずれでも「認証なし」と判定されて、既存サイトは何の影響も受けません。Basic 認証を実際に設定したサイトでだけ user が真値になって、認証経路に乗ります。
加えて、Basic 認証のパスワードは WordPress 管理者パスワードと同じく Fernet 暗号化で保存する設計にしました。ENCRYPTED_SITE_KEYS 定数に 'basic_auth_password' を追加するだけで、保存・読み込み時に自動で暗号化・復号されます。
両経路への配線
ヘルパーが揃ったところで、各呼び出し箇所を配線します。
Playwright 経路 には _new_context_with_auth(browser, site) という共通ヘルパーを 1 つ作って、new_context() を呼んでいた 3 箇所(visual check / thumbnail 取得 / browser residual update)を一括置換しました。
def _new_context_with_auth(browser, site):
http_credentials = get_playwright_http_credentials(site)
if http_credentials:
return browser.new_context(http_credentials=http_credentials)
return browser.new_context()
urllib 経路 は _http_status_check(url, basic_auth=None) の関数シグネチャに basic_auth 引数を追加し、内部で Authorization ヘッダーを送信。これにより「ベースライン 401 → 更新後 401」の偽陰性が消えて、本来の HTTP ステータス(認証突破後の 200/5xx)で正しくロールバック判定できるようになりました。
メンテナンス本体の run_ssh_maintenance 内では、site から _basic_auth を一度だけ取り出して、5 箇所の _http_status_check_stable() 呼び出しに引き継ぐ形にしています。毎回 site dict から取り出すと「ある箇所だけ取り出し忘れる」事故が起きるので、ローカル変数化が安全策。
UI — 一般サイトを圧迫しない折りたたみ
ユーザーから見ると、Basic 認証保護サイトは少数派です。すべてのサイト追加モーダルに常時露出した入力欄を 2 つ並べると、99% のサイトでは無関係なフィールドが場所を取ることになります。
そこで、サイト追加/編集モーダルの WordPress 情報セクション末尾に <details> 要素で デフォルト折り畳まれた「🔐 Basic 認証(任意)」セクション を追加しました。
<details>
<summary>🔐 Basic 認証(任意)</summary>
<input name="basic_auth_user" placeholder="認証ユーザー名">
<input type="password" name="basic_auth_password"
placeholder="認証パスワード">
</details>
ベーシック認証保護サイトのみ展開して入力する設計で、一般サイトの UI は何も変わりません。保存時にパスワードは ENC: プレフィックス付きで Fernet 暗号化されます。
まとめ — 「同じ認証情報を別経路に渡す」設計の引き出し
このラウンドから取り出せる原則は次の 3 つでした。
- 複数経路に同じ認証情報を渡す時はヘルパーに集約する — Playwright 形式・urllib 形式・bool 判定など、利用者ごとに必要な形が違っても、
(user, password)という共通の起点関数からすべて派生させる設計にすると、形式ズレの事故が起こらない - 既存データに新フィールドを足す時は空フォールバックパターン —
site.get(key) or ''で「キー無し = 認証なし」と読み替えれば、既存サイトの挙動を 1 mm も変えずに新機能を上乗せできる。マイグレーション不要 - 少数派の機能は折り畳みで隠す — 99% の利用者に関係ないフィールドを常時露出させると UI が圧迫される。
<details>折り畳み + 控えめなアイコンで「任意機能」を表現すると、必要なユーザーだけ展開する流れを作れる
ステージング環境を Basic 認証で守っている運用は WordPress 界隈では珍しくありません。自動化ツール側で「半分動く」状態が続いていると、結局そのサイトだけは手動運用に逃げざるを得なくなる。全機能を一気通貫で動かすために、認証経路の対応を 1 度きちんと設計しておく価値はあると思います。