社内マニュアルを WordPress で管理できれば、書く側も読む側も楽になる。ただし社外に出せる情報ばかりではない。社内限定のまま、出張先や自宅からも会社の Google アカウントで入れる形を、Cloudflare Access で組んでみた。結論を先に書く。入口に認証の壁を一枚立てるだけでは足りない。内側の WordPress が自分でトークンの署名を検証し、入口を迂回する経路を塞ぐところまでで一組である。
社内マニュアルを WordPress で管理するために必要な要件
社内マニュアルの置き場は、案外決まらない。共有フォルダに文書を置く、表計算に手順を並べる、社内 Wiki を立てる、オンラインのドキュメントに集める。どれも一長一短で、しばらくすると「どこに何があるか分からない」に戻っていく。
WordPress を選ぶ理由は、書く人が書き慣れていることにある。見出しを立て、リンクを張り、画像を置く。編集の履歴が残り、URL で参照できる。マニュアルは更新され続けるものだから、更新のしやすさはそのまま運用の寿命になる。自前のサーバーで WordPress を動かすと、パーマリンクの設定などで一度はつまずく(さくらのVPSでWordPressのパーマリンクが404になる件)。それでも、書く側の負担が軽いことの価値は大きい。
問題は公開範囲である。要件を並べると、三つになる。
- 社内の人間だけが読めること。不特定多数に公開しない。
- 出張先や自宅からも読めること。社内ネットワークの中に閉じない。
- 認証は会社の Google アカウントで済ませること。アカウントを増やさない。
三つ目が、後から効いてくる。社内サーバーを社内ネットワークに置くだけなら話は簡単だが、「外からも使いたい」が加わった瞬間に、境界の内側は安全である、という前提が崩れる。ここで方式の選択が要る。
WordPress を社内限定公開にする方法 — IP 制限・VPN・ゼロトラストの比較
方式は大きく三つある。それぞれ、外から使えるか、認証の管理がどうなるかが違う。
| 方式 | 外から使えるか | アカウント管理 | 向く場面 |
|---|---|---|---|
| IP アドレス制限 | 社内ネットワークの外からは不可 | 追加なし | 拠点が固定で、外から使わない |
| VPN | 可(接続後に利用) | VPN のアカウントが別に要る | 拠点間接続、既に VPN がある |
| ゼロトラスト型の入口 | 可 | Google アカウントをそのまま使える | 外からも使いたい/管理を増やしたくない |
IP アドレス制限は最も簡単である。Web サーバー側で許可するアドレスを並べるだけなので、仕組みも増えない。代償は、社内ネットワークの外に出た瞬間に使えなくなることだ。出張先や自宅から開けない。拠点が固定で、外から使う予定が無いなら、これで足りる。
VPN は外から使える。ただし、VPN のアカウントが別に要る。社内システムを一つ見るために全社のネットワークへ入る、という設計は重い。VPN の限界については、以前VPNはオワコンなのか?今の時代だからこそおすすめしたいワケとはで整理したが、入口を一つ守るために網全体を開ける、という構図は、守る範囲を広げる方向に働く。
ゼロトラスト型の入口は、アプリ単位で認証をかける。利用者は、会社の Google アカウントでログインするだけになる。パスワードは配らないし、覚えなくてよい。やめる人が出たら、Google 側のアカウントを止めれば入口が閉じる。今回はこれを選んだ。
Cloudflare Access と Google アカウント認証を選んだ理由
理由は三つある。第一に、社内ドメインのアカウントだけに絞る、という条件を設定側で一行で書けること。第二に、利用者から見た手間が増えないこと。第三に、外から使う前提の設計を、後から足すのではなく最初から持てることである。
「ゼロトラスト」という語を、ここでは境界の内側を無条件に信頼しない、という意味で使っている。社内ネットワークから来たから安全、という前提を置かない。どの経路から来ても、身元の確認は毎回行う。マニュアルの置き場としては、そのほうが素直である。
セキュリティの担当者が専任で付かない小さな組織ほど、この形は効く。専用のアカウントを配る運用は、作るときより、止めるときに事故が出る。入口を Google アカウントに寄せておけば、入退社の手続きと認証の管理が同じ場所にまとまる。
Cloudflare Access の SSO を WordPress に通す仕組み
入口で本人確認が済んでいるなら、その先のアプリでも同じ本人だと分かっているはずだ。なのに別のパスワードを聞かれる。この二度手間をどう畳むかが、実装の中心になる。

図1: 全体構成。利用者は Cloudflare Access の入口を通り、Google アカウントで認証される。Cloudflare Tunnel を経由して、インバウンドを開放していないオリジンの WordPress に到達する。
Cloudflare Access は、認証を通った利用者に、身元情報を含む署名付きのトークン(JWT)を渡す。このトークンはリクエストのヘッダに載って内側のアプリへ届く。アプリ側は、そのトークンの署名を検証できれば、身元を信頼できる。
大事なのは、アプリが署名を自分で検証することである。ヘッダに載っているから信じる、では駄目だ。ヘッダは偽装できる。Cloudflare が署名した事実を、アプリ自身が確かめる。ここを省くと、入口を迂回して直接アプリを叩く経路が穴になる。流れはこうなる。アクセスが来る。ヘッダからトークンを取り出す。署名と有効期限を検証する。通れば、トークンに書かれたメールアドレスを信頼してよい身元とする。通らなければ拒否する。順序としてはこれだけである。

図2: 認証から WordPress ログインまでの流れ。アプリが信頼してよいのは、署名を自分で検証できたトークンだけである。
公開鍵(JWKS)は取得してキャッシュする
署名の検証には、Cloudflare が公開している検証用の鍵(JWKS)が要る。チームドメインの決まった場所から取得できる。
鍵は固定ではない。定期的に更新される。だから、取得した鍵は一定時間キャッシュする。アクセスのたびに外部へ問い合わせていたら、遅くなるし、外部が落ちれば内側も止まる。キャッシュした鍵で検証し、期限が切れたら取り直す。
鍵は複数返る。更新の過渡期には、古い鍵と新しい鍵が同時に並ぶ。どれか一つで検証が通ればよい。ここを「先頭の鍵だけを見る」と決め打つと、更新の瞬間にだけ落ちる、再現しにくい不具合になる。実際に動かしながら、この点は先に決めておいてよかったと感じた。
発行者・対象者・有効期限は、別々に確かめる
署名の検証では、発行者、対象者、有効期限の三つを確かめる。発行者を見ないと、別のチームが発行したトークンを受け入れてしまう。対象者を見ないと、別のアプリ向けに発行されたトークンが使い回される。有効期限を見ないと、古いトークンが生き残る。
大事なのは、この三つが署名の正しさに吸収されないことである。署名が正しいことは、三つを確かめたことの代わりにならない。どれか一つでも欠ければ穴になる。だから、三つを別々の確認として並べ、それぞれで落ちることを検証の対象にした。
検証は「通らないこと」から先に確かめる
本番のサイトにいきなり入れる前に、検証の道具を用意した。トークンを与えて、検証が通るかどうかだけを確かめる独立のスクリプトである。
ここで確かめたいのは、通ることより、通らないことである。署名を壊したトークン。期限切れのトークン。別のチーム向けのトークン。別のアプリ向けのトークン。ヘッダが無いリクエスト。これらが正しく拒否されることを先に確かめる。拒否の確認が無いと、実は誰でも通る設定だった、という事故が起きる。
鍵の取得とキャッシュも、この段で確かめる。外部から鍵が取れること。キャッシュが効くこと。期限切れで取り直すこと。三つを別々に見ておかないと、キャッシュが効いているのか、単に取り直しに失敗しているのかが区別できない。
WordPress 側の SSO 実装 — プラグインで本体更新と分離する
WordPress への組み込みは、プラグインとして組むことにした。本体に手を入れると、更新のたびに変更が消える。プラグインなら、本体の更新と分離できる。これは運用の都合である。本体の更新は自動で入る。そのたびに手を入れ直す設計は、いずれ忘れられる。
やることは三つである。リクエストのヘッダからトークンを取り出す。それを検証する。検証に通れば、対応するユーザーとしてログインさせる。
差し込む位置と、ユーザーの紐付け
差し込む場所は、認証の判定が始まる前である。画面を作る前、投稿を読む前、いずれの入口よりも先に置く。後ろに置くと、認証されていない状態で一部の処理が走る余地が残る。
ユーザーの紐付けは、トークンのメールアドレスを鍵にする。同じメールアドレスの利用者が既にいれば、その人として通す。いなければ、決められた権限で新しく作る。初回のアクセスで自動的に利用者が増える形である。
自動で作ることに不安はあった。入口で通った任意のアカウントが、そのまま内側の利用者になる。ただし入口の側で社内ドメインに絞ってあるので、通ってくるのはその範囲に限られる。絞り込みの一段目を入口に置き、二段目で受け入れる形にした。同じ確認を二重に持つのではなく、役割を分けて持つ。
権限は既定で最小にする
権限は既定で最も弱いものにした。入口を通ったからといって、いきなり強い権限を与えない。入口の認証と、アプリ内の権限は別の軸である。混ぜてはいけない。この二つを混ぜると、入口の設定を一つ変えただけで、内側の権限が広がる事故が起きる。
本体を直さない理由をもう少し書いておく。自前で持っているシステムほど、認証の改修を本体に埋め込みたくなる。呼び出しの順序を制御しやすいからである。だが、本体に埋め込むと、その変更は本体の一部になる。本体が更新されれば、差分を追い直す。別の管理者が設定画面から別の認証を足せば、順序が変わる。認証は、他の変更に巻き込まれてはいけない部分である。
外に出す代償は、差し込む場所を自分で選べなくなることだ。フックの位置が期待と違えば、認証の前後関係がずれる。だから、差し込む場所が期待どおりかを先に確認した。ここは実際に確かめないと分からない部分だった。
入口の迂回を塞ぐ — Cloudflare Tunnel でオリジンを保護する
署名の検証を入れても、それだけでは穴が残る。アプリの URL を直接叩ける経路があるなら、ヘッダを付けずにアクセスすればよい。検証は「トークンが無ければ拒否」で成り立つが、無いことと偽物であることを、アプリ側だけで完全に区別するのは難しい。
そこで、入口以外からアプリへ到達できないようにする。Cloudflare のトンネル経由に限定し、オリジンにはトンネル以外から接続できないようにする。あるいは、Cloudflare からのリクエストであることを示す証明書を要求する。入口の壁と、直接経路を塞ぐ壁は、別々に要る。

図3: 壁①(入口の認証)と壁②(経路の限定)は役割が違う。片方だけでは、迂回の経路が穴として残る。
この点は、検証環境では見落としやすい。検証環境は外から叩かれないので、直接経路が開いていても気づかない。本番へ移すときに初めて、塞ぎ忘れが問題になる。だから、検証の段階で塞ぎ方まで決めておく。動くことの確認と、穴が無いことの確認は、別の作業である。
公開鍵が取れないときの挙動 — 可用性と安全の設計
運用を考えると、いちばん判断が要るのはここである。外部から鍵が取れないとき、アクセスを通すのか、止めるのか。
通す側に倒せば、外部が一時的に落ちただけで全員が通れる状態が生まれる。止める側に倒せば、外部が落ちただけで全員が締め出される。どちらも困る。社内マニュアルは、止まると仕事が止まる種類のシステムである。
私の判断は、キャッシュの範囲では止めない、キャッシュが切れたら止める、である。直前に取得した鍵がまだ有効なら、それで検証を続ける。鍵の更新間隔に対して、キャッシュの保持時間を十分に長く取っておけば、外部の一時的な不調は吸収できる。逆に、キャッシュが切れて検証の根拠が無くなったら、通してはいけない。根拠の無い通過を作らない。
この設計は、可用性と安全のどちらかを選ぶ話ではなく、時間の余裕で両方を取る話である。鍵が十分に長く生きていれば、短い障害は問題にならない。だから、鍵の寿命とキャッシュの寿命の関係を先に決めておく。
利用者紐付けと権限の持ち方 — メールアドレス変更の扱い
紐付けをメールアドレスで行う以上、その値が変わったときの扱いを決めておく必要がある。
考え方は二つある。変わった時点で別の利用者として扱うか、同一の利用者として追随させるかである。別の利用者として扱えば、古いアカウントが残る。権限が付いていれば、それが残ることになる。追随させれば、権限は引き継がれる。
いまは、追随は自動では行わず、古い紐付けを残したまま新しい値で入れる形にしている。自動で寄せると、別人の権限を引き継ぐ余地が生まれる。人の異動とメールの変更は、必ずしも一致しない。運用で確認してから寄せるほうが安全だと考えている。ここも、実験の段階では決めきれていない部分である。
同じ問題は、複数のアプリで同じ利用者を扱うときに再び現れる。権限をどこに持たせるかを決めないまま増やすと、後から寄せ直す作業が重くなる。
認証ログに残すもの — WordPress 社内公開の監査
認証の仕組みは、通ったかどうかだけでは足りない。誰が、いつ、どの入口を通って、どこで弾かれたかが要る。
残すのは、認証の成否、身元として採用した値、弾いた理由である。トークンそのものは残さない。トークンは有効期限内は資格情報であり、ログに置けば漏えいの経路になる。残すのは判定の結果とその理由である。
弾いた理由を残すのは、設定の誤りを見つけるためでもある。拒否が続いているのに原因が分からない、という状態を避けたい。理由が分かれば、鍵の取得に失敗しているのか、対象者が違うのかが切り分けられる。
まとめ — WordPress を社内限定公開にする手順と残る運用課題
この記事で扱った内容を、要点だけ並べておく。
- 社内マニュアルを WordPress で管理するなら、公開範囲の要件を先に決める。社内限定、外からも使える、Google アカウントで認証、の三つである。
- 外から使う要件があるなら、IP 制限では足りない。VPN は使えるが、アカウントが別に増える。ゼロトラスト型の入口は、既存の Google アカウントをそのまま使える。
- 入口の認証だけでは足りない。内側の WordPress が JWT の署名を自分で検証する。
- 公開鍵はキャッシュし、発行者・対象者・有効期限を別々に確かめる。
- 組み込みはプラグインで行い、本体の更新から分離する。
- 通ることより、通らないことを先に確かめる。
- 入口を迂回する経路を塞ぐ。トンネル経由に限定する、証明書を要求する、いずれかが必要である。
- 公開鍵が取れないときの挙動は、時間の余裕で設計する。
入口の壁を一枚立てるだけでは、内側は守られない。壁を通った先の身元を内側へ正しく伝えるまでが、認証の設計だと考えている。認証は入口で終わるのではなく、内側のアプリが身元を信頼できるところまで続く。
まだ検証の段階であり、本番の運用に載せるまでに詰める点は残っている。入口で拒否された利用者と、入口は通ったがアプリ側で弾かれた利用者を別の事象として記録すること。複数のアプリで同じ利用者を扱うときの権限の持ち方。鍵の更新に追随できているかをどう監視するか。このうち次に着手するとすれば、権限の持ち方である。認証の透過は入口と内側をつなぐ話だが、権限は利用者とアプリの関係の話であり、同じ「通す」でも設計の軸が違う。ここを混ぜると、入口の設定を変えただけで内側の権限が動く事故につながる。順序を分けて確かめたい。