14 KiB
14 KiB
CertRemind 開発ガイド
概要
- アプリケーション名: CertRemind
- 目的: ユーザーが登録した Web サイトの TLS/SSL 証明書の有効期限を監視し、期限切れ前にアプリ内アラート、Webhook、Push 通知で知らせる。
- 現在の実装状況:
development_plan.mdのフェーズ 8 までの機能を実装済み。MVP の主要機能に加え、基本的な品質向上、テスト、運用準備も整備済み。
技術スタック
- Node.js v22
- pnpm
- Hono.js
- React.js / Vite
- Radix UI
- lucide-react
- PostgreSQL
- Docker Compose
- OpenSSL
- Argon2id
- TOTP:
otplib - Push 通知:
web-push - QR コード:
qrcode.react - テスト: Vitest
- Lint / Format: ESLint / Prettier
主要コマンド
pnpm install
docker compose up -d postgres
docker compose up -d --build
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
pnpm dev
pnpm monitor:once
pnpm monitor:worker
pnpm lint
pnpm test
pnpm exec vite build
pnpm format
開発サーバー:
Frontend: http://127.0.0.1:5173/
API: http://127.0.0.1:3000
Docker Compose:
本番相当: docker compose up -d --build
開発用: docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
本番 URL: http://127.0.0.1:3000/
開発 URL: http://127.0.0.1:5173/
docker-compose.ymlは本番相当の標準 Compose として、PostgreSQL、Web/API アプリ、monitor-workerを起動する。docker-compose.dev.ymlは開発用 override として、アプリを bind mount しpnpm devで Vite と API を起動する。- Docker CLI が Compose v2 サブコマンドに対応していない環境では、同じ引数で
docker-composeを使う。 - Compose のアプリ公開ポートは
127.0.0.1に限定し、外部 NIC へ公開しない。 - Compose の PostgreSQL はホストに公開せず、
app/monitor-workerから内部ネットワーク経由でのみアクセスする。 - コンテナ内では
HOST=0.0.0.0を使い、ホスト側への公開範囲は Compose のportsで制御する。
ディレクトリ構成
db/schema.sql
Dockerfile
docker-compose.yml
docker-compose.dev.yml
README.md
public/push-sw.js
src/client
src/client/api/client.js
src/client/components
src/client/components/Toast.jsx
src/client/routes
src/client/styles/app.css
src/server
src/server/app.js
src/server/index.js
src/server/config/env.js
src/server/db/pool.js
src/server/middleware
src/server/modules
src/server/jobs/monitorCertificates.js
src/server/jobs/monitorWorker.js
src/server/utils/logger.js
tests/apiSecurity.test.js
tests/monitoring.test.js
tests/urlPolicy.test.js
環境変数
.env.example を参照すること。
主な項目:
NODE_ENVHOSTPORTDATABASE_URLCOOKIE_SECRETVAPID_PUBLIC_KEYVAPID_PRIVATE_KEYVAPID_SUBJECTOPENSSL_PATH
補足:
HOST未設定時は127.0.0.1で待ち受ける。Docker Compose ではコンテナ内到達性のため0.0.0.0を指定する。- ホスト実行時の
DATABASE_URLは通常localhost:5432、Compose 内のサービスではpostgres:5432を使う。ただし Compose 起動中の PostgreSQL はホストに公開しない。 OPENSSL_PATH未設定時はopensslを使う。- Windows では Git 付属の
openssl.exeを自動検出する実装がある。 - VAPID private key が未設定の場合、Push 実送信は失敗として
delivery_resultに記録される。
データベース
DDL は db/schema.sql に保管している。
実装済みテーブル:
usersuser_totpsessionssitesnotification_methodssite_alert_conditionsalert_history
設計メモ:
- 主キーは UUID。
- 全テーブルに
created_at/updated_atを持つ。 updated_atはトリガーで更新する。sitesは最新の証明書期限、確認日時、取得失敗内容を保持する。- ユーザー関連データは
users削除を起点に CASCADE で削除する。 - サイト削除時、通知条件は CASCADE で削除する。
- アラート履歴の
site_idはサイト削除時にNULLへ変更する。 - アラート重複抑制は
alert_history.dedupe_keyを使う。
注意:
docker-compose.ymlは初回 DB 起動時にdb/schema.sqlを読み込む。既存 volume がある場合、schema の変更は自動再適用されない。docker-compose.ymlはアプリ本体と監視 worker も起動するため、DB だけを起動したい場合はdocker compose up -d postgresを使う。- 開発用 Compose は
docker-compose.dev.ymlを重ねて使い、Vite の公開ポートも127.0.0.1:5173に限定する。 - Compose の PostgreSQL へホストから直接接続する前提の作業は行わない。DB 操作が必要な場合はコンテナ内実行や一時的な明示設定を検討する。
実装済み API
GET /api/health
GET /api/auth/csrf
POST /api/auth/register
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me
GET /api/sites
POST /api/sites
GET /api/sites/:siteId
PATCH /api/sites/:siteId
DELETE /api/sites/:siteId
GET /api/sites/:siteId/settings
PUT /api/sites/:siteId/settings
DELETE /api/sites/:siteId/settings
GET /api/alerts
PATCH /api/alerts/:alertId/read
DELETE /api/alerts/:alertId
GET /api/notification-methods
POST /api/notification-methods/webhooks
PATCH /api/notification-methods/webhooks/:methodId
DELETE /api/notification-methods/webhooks/:methodId
POST /api/notification-methods/push-subscriptions
GET /api/account
PATCH /api/account/profile
PATCH /api/account/password
POST /api/account/totp/setup
POST /api/account/totp/verify
DELETE /api/account/totp
DELETE /api/account
実装済み画面
認証前:
- 登録画面
- ログイン画面
- 2 段階認証コード入力
認証後:
- URL ルーティング
/sites/sites/:siteId/settings/alerts/notifications/account/login/register- 直リンク、初期読込み、ブラウザバック / フォワードに対応
- 未認証で保護画面 URL を開いた場合はログイン画面を表示し、ログイン後に元の画面へ復帰する
- 左サイドメニュー
- PC 幅では展開表示
- 720px 以下ではアイコンのみの畳み表示
- サイト一覧、アラート履歴、通知方法、アカウント、ログアウトに対応
- 共通トースト通知
- 認証後画面の正常メッセージ、エラーメッセージを右上に表示
- 5 秒後の自動非表示と手動クローズに対応
- ログイン / 登録画面のエラーはインライン表示を維持
- サイト一覧
- サイト追加
- 最新監視結果に基づく証明書期限表示
- 確認ダイアログ付きサイト削除
- サイト設定
- エイリアス編集
- 通知タイミング設定
- 時間 / 日 / 週間の単位指定
- 複数タイミングの追加
- 確認ダイアログ付き通知タイミング削除
- アプリ内アラート必須表示
- Webhook 選択
- Push 通知フラグ設定
- 確認ダイアログ付き設定削除
- アラート一覧
- サイト絞り込み
- アラート種類絞り込み
- 開始日時 / 終了日時絞り込み
- 既読更新
- 確認ダイアログ付き履歴削除
- 通知方法管理
- Webhook 登録
- Webhook 編集
- 確認ダイアログ付き Webhook 削除
- ブラウザ Push 通知の許可状態表示
- VAPID public key がある場合の Push 購読登録
- アカウント設定
- 表示名更新
- ダイアログでのパスワード更新
- パスワード更新後の全セッション無効化
- ステップ式ポップアップでの 2 段階認証セットアップ
- 2 段階認証 QR コード表示
- シークレット文字列表示
- 確認ダイアログ付き 2 段階認証解除
- 確認ダイアログ付きアカウント削除
入力検証
サーバー側:
- Zod でリクエストを検証する。
- クライアントから送られる
user_idは信用しない。 - 認証済み API はセッションのユーザー ID を使う。
- SQL はプレースホルダを使う。
クライアント側:
- 登録 / ログイン、サイト、Webhook、アラート絞り込み、アカウント設定で入力必須チェックと形式チェックを実装済み。
- クライアント側検証は UX 用であり、サーバー側検証を省略してはいけない。
認証・セッション
- パスワードは Argon2id でハッシュ化する。
- セッションは
sessionsテーブルで管理する。 - セッション Cookie は HttpOnly / SameSite=Lax。
- 本番環境では Secure Cookie を有効化する。
- CSRF トークンを状態変更 API で必須にする。
- パスワード更新時は対象ユーザーの全セッションを削除し、現在の Cookie も削除する。
- TOTP が有効なユーザーはログイン時に OTP 検証が必須。
サイト管理
- サイト URL は HTTPS のみ許可する。
- URL は
normalizeHttpsUrlで正規化する。 localhost、private IPv4、loopback IPv4 は拒否する。- サイト API は常にセッションユーザーの所有データのみ扱う。
- サイト設定の通知タイミングは内部的に時間単位で保存する。
- 通知タイミングは 1 時間以上、17520 時間以内。
- 同じサイトに同じ通知タイミングを重複登録できない。
アラート履歴
- アラート履歴は
alert_historyに保存する。 - アラート一覧はログインユーザーの履歴のみ返す。
- 既読更新、削除もログインユーザーの履歴のみ対象にする。
- 絞り込み条件:
- サイト
- アラート種類
- 開始日時
- 終了日時
- アラート重複抑制には
dedupe_keyを使う。
通知方法
Webhook:
- HTTPS のみ許可する。
normalizeHttpsUrlを通し、localhost / private IPv4 / loopback IPv4 を拒否する。- Slack 互換 Webhook として送信する。
- 更新・削除はログインユーザーの通知方法のみ対象。
Push:
- ブラウザ Push 購読情報を
notification_methodsに保存する。 - endpoint は HTTPS のみ許可する。
- Service Worker は
public/push-sw.js。 - VAPID key は環境変数で管理する。
証明書監視ジョブ
一回実行:
pnpm monitor:once
定期実行 worker:
pnpm monitor:worker
処理内容:
- 登録サイトを取得する。
- OpenSSL で証明書期限を取得する。
- 成功時は
sites.certificate_expires_at/certificate_checked_atを更新し、取得失敗内容をクリアする。 - 失敗時は
sites.certificate_checked_at/certificate_check_errorを更新し、既存の期限値は残す。 - サイトごとの通知条件を評価する。
- 条件一致時にアラート履歴を作成する。
- Webhook / Push 通知を送信する。
- 証明書取得失敗も
certificate_check_failedとしてアラート化する。 - サイト単位の失敗で全体を止めない。
- 外部通信を伴う監視処理は並列数を制限する。
- Webhook / Push の送信失敗は
delivery_resultに記録する。 - 監視ジョブの開始、終了、失敗は構造化ログで出力する。
pnpm monitor:workerは 1 時間ごとに監視ジョブを実行する長時間起動プロセス。- 本番相当の Docker Compose では
monitor-workerサービスとしてpnpm monitor:workerを常駐起動する。
注意:
- サンドボックス内では外部 TLS 接続が
Permission deniedになる場合がある。その場合は権限付き実行が必要。 - 定期実行は
pnpm monitor:workerで提供済み。運用要件に応じて cron やタスクスケジューラからpnpm monitor:onceを実行する構成も選択できる。
セキュリティ方針
- リクエスト改ざんに注意する。
- クライアントから送信されてきたデータを信頼しない。
- CSRF トークンを使用する。
- パスワードを平文保存しない。
- SQL インジェクションを避けるためプレースホルダを使う。
- 認可はセッションユーザーを基準に判定する。
- URL ルーティングは表示状態の復元用であり、認可は必ず API 側のセッションユーザー基準で判定する。
- 未認証時の復帰先 URL はアプリ内の許可済みパスのみ扱い、外部 URL へのリダイレクトに使わない。
- 1 リクエストがサーバー全体の処理をブロックしないようにする。
- 外部通信はタイムアウトや並列数制限を設定する。
- 削除操作には確認ダイアログを置く。
- API の未捕捉エラーは構造化ログで記録する。
テスト・運用準備
tests/urlPolicy.test.jsで URL 正規化と基本的な SSRF 対策を検証する。tests/apiSecurity.test.jsで CSRF、認証必須、セッションユーザー基準の認可、Webhook URL 検証、通知条件の所有者確認を検証する。tests/monitoring.test.jsで証明書監視成功 / 失敗時の DB 更新、アラート作成、通知呼び出しを検証する。README.mdに開発環境起動手順、検証コマンド、DB 注意点、環境変数一覧を記載済み。- サンドボックス環境では Vite / Vitest の設定解決で権限付き実行が必要になる場合がある。
既知の強化候補
- API 単体テスト / 統合テストをさらに増やす。
- ログイン試行回数制限を追加する。
- SSRF 対策として DNS 解決後の IP チェックを追加する。
- IPv6 private / link-local / unique local address の検証を追加する。
- セッションローテーションを追加する。
- E2E テストを追加する。
開発時の注意
development_status.mdは作業後に更新する。- DB 変更を行う場合は
db/schema.sqlも更新する。 - API を追加したら、認証、CSRF、認可、入力検証を確認する。
- UI の削除操作には確認ダイアログを追加する。
- 既存の左サイドメニューとレスポンシブ挙動を崩さない。
- フロントエンドの入力チェックを追加しても、サーバー側検証を必ず維持する。
- Docker Compose のアプリポートを変更する場合も、ホスト側 bind は
127.0.0.1に限定する。 - 変更後は少なくとも以下を実行する。
pnpm lint
pnpm test
pnpm exec vite build