$ cat posts/20260711-mnvnv7.mdx
静的サイトに「管理者だけ入れるページ」を作る — Cloudflare Accessでコード0行の認証
はじめに
このブログはNext.jsの静的エクスポート(output: 'export')をCloudflare Pagesに置いているだけの構成で、サーバーが存在しない。そこに「管理者(自分)だけが見られるページ」を作りたくなった。
サーバーがないということは、セッション管理もパスワード照合もやる場所がないということだ。さてどうするか。
結論: Cloudflare Accessを使うと、アプリのコードを1行も書かずに認証を付けられる。この記事はその仕組みと、導入時に考えたこと・実際に確認したことの記録。
検討した3つの方式
| 方式 | 判断 |
|---|---|
| クライアントサイド認証(JSでパスワード判定) | ✗ 静的サイトではHTMLもJSも全員に配られるので、隠したことにならない |
| Pages Functions / Workersで認証実装 | △ 可能だが、セッション管理と秘密情報の運用を自前で抱えることになる |
| Cloudflare Accessでパス保護 | ◎ エッジで認証。コード不要、無料枠で十分 |
静的サイトの大原則として、ブラウザに届いたものはすべて公開情報だ。認証はブラウザより手前、つまりエッジでやるしかない。Cloudflare PagesはもともとCloudflareのエッジ経由で配信されているので、そこに関所を置けるAccessが構成的にきれいにはまる。
設定はダッシュボードで数分
Zero Trust → Access → Applications で Self-hosted アプリを作り、
- Application domain: サイトのドメイン、Path:
admin - Login method: One-time PIN(メールに6桁コードが届くやつ)
- Policy: Allow / Emails に自分のメールアドレスを1件だけ
これだけ。デプロイや設定ファイルの変更は不要で、既存の静的サイトの/admin/*にだけ認証がかかる。
確認してみると:
$ curl -s -o /dev/null -w '%{http_code}' https://<site>/blog/
200
$ curl -s -o /dev/null -w '%{http_code}' https://<site>/admin/
302 # → <team>.cloudflareaccess.com のログインページへ/adminへのリクエストはオリジン(静的ファイル)に到達する前にエッジで302される。公開ページはそのまま200。
「登録してないメールを入力されたらどうなるの?」
導入して最初に気になったのがこれ。ログイン画面は誰でも開けて、メールアドレスも自由に入力できる。
Accessは2段階でポリシー判定をしている:
- メール入力時: 入力アドレスをAllowポリシーと照合。許可リストにないアドレスにはコードが送られない(またはその場で拒否される)
- コード検証時: 最終的にセッション(JWT Cookie)を発行する時点で再度ポリシー判定。許可外のメールにはセッションが発行されない
つまり突破するには許可されたメールボックス自体を乗っ取るしかない。言い換えると、/adminの強度は自分のメールアカウントの強度と等しくなるので、メール側の2段階認証が実質的な要になる。
実機確認として、シークレットウィンドウから別のメールアドレスを入力して入れないことまで見届けた。「守られているはず」と「守られていることを確認した」は別物なので、この一手間はやる価値がある。
公開リポジトリで何を隠すべきか
このサイトのリポジトリはpublicなので、/adminページのソースコードも誰でも読める。これは問題ない——Accessが守っているのは「デプロイされたURLへのアクセス」であって、ソースの秘匿ではない。だから管理ページに置くのはリンク集のような、読まれても困らないものに限る。
一方で、セキュリティ運用の詳細(レートリミットの閾値、未対応の項目リストなど)は攻撃者へのヒントになりうるので、公開リポジトリのdocsには書かない方がいい。原則はこう整理できる:
- 設計が知られても破れない仕組みを選ぶ(Accessはこれを満たす。security by obscurityにしない)
- その上で、運用の手の内は見せない(隠すことが防御ではないが、わざわざ地図を渡すこともない)
落とし穴: サイト内検索が/adminを晒していた
導入後に気づいた見落としが1つ。このサイトの検索はPagefindで、ビルド時にout/配下の全HTMLからインデックスを作る。/adminも静的ページとして普通にビルドされるので、検索ボックスに「admin」と打つと管理ページが内容ごとヒットしてしまっていた。
Accessが守るのはURLへのアクセスであって、ビルド成果物から作られた検索インデックスは別物、というわけだ。エッジで门番を立てても、検索インデックスという「別の出口」から中身が見えていたら意味がない。
対処は、Pagefindのdata-pagefind-body属性を公開したい記事の<article>にだけ付けること。この属性がサイト内に1つでも存在すると、付いていないページはインデックスから丸ごと除外される仕様なので、/adminやタグページは検索に出なくなる。静的サイトに「見せないページ」を作るときは、本体のアクセス制御だけでなく、検索インデックス・sitemap・RSSといった派生物への漏れまで棚卸しする必要がある。
DDoSはどうする
「大量リクエストが来たら?」も気になったが、調べた結論は「まず何もしなくていい」だった。
- Cloudflare Pagesは無料プランでもL3/L4/L7のDDoS緩和が無制限・常時有効
- Accessを設定した後は、未認証の/adminリクエストはエッジで拒否されてオリジンに届かない
- One-time PINの総当たりもAccess側でレート制限されている
さらに反応的な制御をしたければWAFのRate Limiting Rule(無料プランで1本)が使えるが、これは自分のゾーン(カスタムドメイン)にしか設定できない、という制約だけ覚えておけばいい。
まとめ
- 静的サイトの認証は「ブラウザに届く前」=エッジでやる。Cloudflare Accessならコード0行
- One-time PINの強度は自分のメールアカウントの強度。メール側の2FAが要
- 「守られているはず」で終わらせず、許可外アカウントで入れないことを実機確認する
- publicリポジトリでは「知られても安全な設計」を選んだ上で、運用の手の内は書かない
管理ページ自体はまだリンク集だけの雛形なので、育てていくのはこれから。AccessのポリシーやWAFルールはTerraformでコード管理できるので、そのうちIaC化して別記事にする予定。