対象: 前の記事でSSL強制を試して、「で、運用している人はこれを毎回どう確認するの?」が気になった方
所要時間: 30〜40分 / 使うもの: ブラウザ、ターミナル、Supabaseのプロジェクト(Freeプランで可)
確認日: 2026年10月(画面や料金は変わることがあります)
前の記事(手順編): https://qiita.com/tomozilla/items/c211b4676776bbd61eb3
ビジネス編(やさしい解説・note): https://note.com/tomozilla/n/n11ff9cfb7b67
サンプルコード一式(GitHub): https://github.com/tomozilla/ssl-enforcement-check
記事の中のコードは日本語コメントです。リポジトリの本体は英語コメントで、日本語コメント版(この記事と同じもの)はja/フォルダーにあります。
前の記事では、「SSLを強制」のスイッチを押して、何が起きるかを手作業で確かめました。psql でわざと間違ったパスワードを試したり、SQL Editorで起動時刻を聞いたり。
終わってみて、思いました。これ、毎回やるのは大変だな。そして、これを見たい人は、SQLを書かない人かもしれない。
運用の担当者、情報システムの人、受託開発で「うちのデータ、暗号化されてますか?」とお客さまに聞かれた人。そんな人が見たいのは、きっと1枚の画面です。そこで、前の記事で確かめた3つのこと(暗号化・再起動・止まっていた時間)を、ステータスページにしてみました。
0. 先に伝えておきたいこと(正直ベース)
- 今回のページが見ているのは、アプリ(Edge Function)からデータベースへの接続だけです。外から
psqlなどでつなぐ接続は、前の記事のスクリプトで確かめます(同じ画面では見られません)。 - 「止まっていた時間」は、定期ジョブ(cron)の実行が途切れた時間から求めた目安です。ユーザーが実際に困った時間そのものではありません。
- データは、小さな空のデモプロジェクトのものです。
-
費用:このページは30秒ごとに
security-status関数を呼びます。1つのタブを開きっぱなしにすると、1日に約2,880回、1か月で約8万6千回の呼び出しです(計算した目安)。Freeプランの月50万回(ドキュメントより)に対して、1枚のタブで約17%です。タブを何枚も開きっぱなしにすると、すぐ増えます。 私は実際の使用量は測っていません。他にかかるお金はありません。 - セキュリティ:このページには、データベースの稼働状況が出ます。関数はVerify JWTをオフにして、「アプリのpublishableキー」を確認するだけの簡単な守りにしています。社外に公開するページには、本物のログインを付けてください。
-
未検証:
verify-fullと証明書での接続、大きなデータベースでの再起動、Fast database reboot の実測、スマホでの表示。
1. つくったもの:接続セキュリティ・ステータス
画面は4つのブロックでできています。
| ブロック | 見えること | どこから取る? |
|---|---|---|
| アプリ → データベースの接続 | 暗号化されているか、TLSのバージョン、暗号方式 | pg_stat_ssl |
| データベースの稼働 | 最後の再起動の時刻、稼働時間 | pg_postmaster_start_time() |
| 定期ジョブの実行:直近60分 | 1分ごとの実行回数(棒グラフ) | cron.job_run_details |
| 止まっていた時間帯 | 直近72時間で、実行が25秒より長く途切れた所 | 同上 |
まず驚いたことがあります。前の記事では、「Edge FunctionからDBへの接続は、最初からSSLだと、ドキュメントに書かれていた」とだけ書いて、私は確かめていませんでした。今回、データベース自身に聞いたら、**TLS 1.3、TLS_AES_256_GCM_SHA384(256bit)**と返ってきました。「ドキュメントでは…」が、「私の環境で確かめました」に変わりました。
2. つくり方
API(Edge Function)
security-status は、読むだけの関数です。3つのことを聞きます。
// この関数 → データベースの接続が暗号化されているか(pg_stat_ssl は、自分の接続の状態を見られる)
const [conn] = await sql`
select ssl, version as tls_version, cipher, bits
from pg_stat_ssl where pid = pg_backend_pid()`;
// Postgresがいつ起動したか(=最後の再起動)
const [db] = await sql`
select pg_postmaster_start_time() as started_at, now() as now,
current_setting('server_version') as server_version`;
「止まっていた時間」は、cronの実行履歴から、前の実行との間隔が25秒を超えた所を拾います。10秒ごとのジョブなので、25秒を超えたら、何かあったと考えます。
with runs as (
select start_time, lag(start_time) over (order by start_time) as prev
from cron.job_run_details where start_time > now() - interval '72 hours')
select prev as from_ts, start_time as to_ts,
extract(epoch from (start_time - prev))::int as seconds
from runs where start_time - prev > interval '25 seconds'
order by start_time desc limit 20;
前の記事と同じ考え方(cronはデータベースの中で動くので、DBが止まっていると実行されない)です。ただし、これは10秒ごとのジョブが1つだけある前提です。ジョブが複数あると、間隔の計算がずれます。
関数の入り口では、前の記事のアプリと同じように、publishableキーが付いているかを確認しています(Verify JWTはオフのまま)。
画面
画面は、HTML 1枚です。30秒ごとに関数を呼んで、結果を描き直します。ライブラリは使っていません。棒グラフもSVGを自分で組み立てています。
動かす
- 前の記事のように、10秒ごとのcronジョブを用意します(
sql/00_heartbeat_cron.sql)。 -
security-statusを、Edge Functionとしてデプロイして、Verify JWTはオフにします。 -
app/config.example.jsをconfig.jsにコピーして、URLとpublishableキーを入れます。 - 配信します。
cd app python3 -m http.server 8765 --bind 127.0.0.1 # → http://127.0.0.1:8765/index.html
3. 実際にプロジェクトを再起動してみた
見えるようにしたのなら、再起動で何が見えるかを試したくなりました。ただ、その前に一つ、私は勘違いをしていました。
前の記事で測った「約20秒」は、SSL強制のスイッチで起きた再起動です。ドキュメントには、この設定の変更は「fast database reboot(データベースだけの高速な再起動)」を起こす、と書いてありました。一方、ダッシュボードの Project Settings → General → Restart project は、別物のようです。画面には「Your project will not be available for a few minutes.」とあり、横の ▼ を開くと、こう出ます(2026年10月、私の画面での表示です)。
Fast database reboot
Restarts only the database service, with less downtime than a full project restart. Other project services remain running.
つまり、メインの Restart project は、データベースだけでなくプロジェクト全体(データベースの上で動くAPIやログイン、ストレージなどのサービスも含めた一式)の再起動で、fast database reboot は、それよりも軽い別メニューです。内部で何をどの順番で再起動しているのかは、私が調べた範囲では公開された説明が見つかりませんでした(ここは分かりません)。
| 操作 | 何が再起動するか | 画面・ドキュメントの案内 |
|---|---|---|
| SSL強制のスイッチ | データベースのみ(fast database reboot) | ドキュメント:小さなプロジェクトでは通常数秒、大きいと長くなることも。同じページの別の箇所と確認ダイアログには「数分」とも。 |
| Restart project | プロジェクト全体 | 画面:数分間、プロジェクトが使えない |
| ▼ の Fast database reboot | データベースのみ | Restart projectより短いダウンタイム |
そこで、Restart project のほうを押して、外から10秒に1回くらいの間隔で security-status を呼び続けました。
| 経過 | 結果 |
|---|---|
| 〜3秒 | 正常 |
| 7秒 | エラー(HTTP 500) |
| 32秒 | 応答なし(タイムアウト) |
| 67秒 | エラー(HTTP 500) |
| 74秒 | 正常。起動時刻が新しくなっていた |
応答が戻るまで、約70秒でした。ステータスページの「止まっていた時間帯」には、こう出ました。
| 回 | きっかけ | ページが示した空白 |
|---|---|---|
| 1回目 | 前の記事でSSL強制のスイッチ(fast database reboot) | 約20秒(前の記事の手作業の測定) |
| 2回目 | 今回、Restart project | 1分17秒 |
| 3回目 | 今回、もう一度 Restart project | 1分29秒 |
1回目と2・3回目は、同じ「再起動」でも別の操作です。最初は「約20秒は運が良かった1回」と考えていましたが、データベースだけの再起動と、全体の再起動の違いのほうが、ありそうな説明です。ただし、▼ の Fast database reboot を、私は自分では測っていません。SSL強制のスイッチが fast database reboot だというのも、ドキュメントの記述からの推測で、画面で確認したわけではありません。どれも小さな空のデモでの1〜2回の結果で、本番のデータ量では変わるはずです。
本番で選ぶなら、「データベースの設定を変えたいだけ」のときは fast database reboot、「全体を立ち上げ直したい」ときだけ Restart project、と使い分けられそうです(私は試していません)。どちらも、止まっている間は security-status のようなAPIも応答しません。
4. 見つけた問題と、直したところ
① 障害のとき、古い表示が「いまの状態」のように見えた
再起動の最中に、ページを更新してみました。
上にはエラーが出ていますが、その下は**ふだんどおり「暗号化されています」「稼働 6分18秒」**のままです。これは状態を伝えるページとして、まずいです。 止まっているのに、緑の丸が出たままで、稼働時間も止まったまま。見た人は「大丈夫そう」と思ってしまいます。
直し方は、取得に失敗したら、表示を灰色にして、「何時のものか」を書くことです。
} catch (e) {
showErr('状態を読めませんでした:' + e.message + (lastOk ? `。下の表示は ${時刻} 時点の古い情報です。` : ''))
if (lastOk) document.querySelector('main').classList.add('stale') // 灰色にする
}
直したあとの画面です(実際の再起動中ではなく、通信を止めて、同じ状況をつくって撮りました)。
② 古い再起動は「原因は不明」になる
画面の「止まっていた時間帯」で、いちばん新しい再起動だけが「データベースの再起動」と判定されています。2回目の空白は、「不明」です。
原因は、Postgresが教えてくれる起動時刻が最後の1回分だけだからです。過去の再起動の時刻は、どこにも残っていません。ページの注意書きにも、そう書きました。
ほんとうは、定期的に起動時刻を表に記録しておけば、過去の再起動も判定できそうです(私は試していません)。
③ 「不明」の空白は、私の作業だった
2回目より前の「3分3秒」の空白は、この日、私がcronジョブを消して作り直した時間でした。ステータスページは、それが「再起動」ではなく「ジョブの停止・作り直し」かもしれない、ということまでは分かりません。空白の原因は、ページだけでは決められない、というのが正直なところです。
④ 再起動の直後は、丸を橙にした
起動してから5分未満のときは、稼働時間の丸を橙にして、「再起動して間もありません」と出しました(上の画像)。
5. 実際のWebアプリ(運用画面)にするなら
どれも、私は試していません。
- ログイン:今は、キーを知っていれば誰でも見られます。社内向けなら、ログインした人だけに。
- 過去の記録:起動時刻や、空白を表に残すと、半年前の再起動も振り返れます。
- 通知:空白や「暗号化されていない」を見つけたら、メッセージで知らせる。
- 外からの確認:「SSLなしでの接続が断られること」を、サーバー側から定期的に試して、結果を表示する。
- ジョブが複数ある場合:ジョブごとに間隔を見る。
まとめ
- 暗号化・再起動・止まっていた時間を、1枚のステータスページにまとめられた
- データベース自身に聞くと、TLS 1.3で接続していることが確認できた(ドキュメントの説明を、自分の環境で確かめられた)
- Restart project(プロジェクト全体の再起動)では、応答が戻るまで約70秒。止まっていた時間帯は、1分17秒・1分29秒。前の記事の約20秒は、データベースだけの再起動(SSL強制のスイッチ)で、別の操作だった
- 状態のページは、失敗したときの見せ方が大事。古い表示を、そのまま出さない
- 空白の原因は、ページだけでは決められないことがある
コードは、上のGitHubにあります。次は、「会社ごとに見えるデータを分ける」(RLS)を、ログインできる請求書ポータルにしてみます。
スクリーンショットは、記事用に作った無料プランのデモ環境で撮影したものです。




