Hamdy Tabsissi
日本語

このウェブサイト 本番稼働中 個人プロジェクト

私が運用し、その場で確かめられる一つのシステム

いまご覧のサイトは、私自身が設定し、守り、更新し、監視しているサーバーで動いています。空のマシンから、このページまで。

ニーズ

ポートフォリオは、スキルを並べるだけでなく証明するべきものです。マネージドなサービスに載せてしまうと、見せたいもの、つまり信頼できる安全なサービスを長く運用する力が、かえって見えなくなります。

アイデア

ホスティングそのものを実演にすること。ここに書いたことはすべて、このページを配信しているマシンの上で、その場で確かめられます。

つくったもの

  • パリリージョンのクラウドサーバー。無料枠の空きが少なく、数日かけて6回目の挑戦でようやく確保しました。
  • Web の構成一式。各サービスを制限された専用の領域で動かし、ひとつの不具合がほかに広がらないようにしています。
  • 最初の夜からセキュリティ強化を開始しました。1晩で7つの対策を無停止で適用し、その後も段階的に進めました。
  • 自動更新:承認された変更はひとりでに反映され、動作確認を経て、新しいバージョンが応答しなければ自動で元に戻ります。
  • データベースの継続的なバックアップに加え、毎晩のスナップショットを日・週・月単位で保管。
  • サイトのセキュリティを1時間ごとに点検し、結果を公開する独立したプローブ。下に表示しています。

成果

  • 5別のプローブが1時間ごとに再実行するセキュリティチェック
  • 7ひと晩で適用、停止時間ゼロ
  • 6サーバー確保までの試行回数
  • 1時間独立したセキュリティ点検の間隔

2026年8月から、EventMap とともにこのポートフォリオを配信しています。下のリアルタイム点検は、独立したプローブが測定したもので、手で書いたものではありません。

リアルタイム点検

  • TLS 確認済み
  • オリジン 確認済み
  • ヘッダー 確認済み
  • DNSSEC 確認済み
  • 稼働率 確認済み

これらの点検は、独立したプローブがサイトの公開インターフェースを通じて1時間ごとに測定しています。手で書いた値ではありません。 最終実行: 5つの点検の詳細(フランス語)

画面で見る

このサイトのトップページ:星空と小さな軌道の図を背景に「ニーズを、役立つシステムに変えます。」の文字
このサイトのトップページ:星空と小さな軌道の図を背景に「ニーズを、役立つシステムに変えます。」の文字

学んだこと

マシンの中からだけでテストしないこと。ファイアウォールが二重になっていると、ローカルでは問題なく応答するのに、インターネットからは誰もアクセスできない、ということが起こります。そして、何かを入れないと決めること(今回はデータベースサーバー)も、運用のスキルです。

技術的な詳細 技術スタック、アーキテクチャ、設計判断(技術職の方向け)

技術スタック

  • Ubuntu 22.04
  • Oracle Cloud ARM (Always Free)
  • nginx
  • Node.js + Express
  • SQLite (node:sqlite)
  • systemd
  • fail2ban
  • iptables
  • Cloudflare
  • GitHub Actions
  • Litestream + restic

アーキテクチャの全体像

  1. 訪問者のブラウザ
  2. Cloudflare — TLS とキャッシュ
  3. nginx — セキュリティヘッダー、レート制限
  4. Node.js + Express — サーバー側でページを生成、7言語に対応。サンドボックス化した systemd サービス
  5. SQLite (node:sqlite) — プロジェクトと自前のアクセス記録
  6. 周辺の仕組み:GitHub Actions がすべての変更をテストし、VM は検証済みのリリースを取得して /health を確認、失敗すればロールバックします。セキュリティ・オブザーバトリーが30分ごとにセキュリティを再点検します。

仕組み

  • 4 OCPU / 24 GB の VM(Ubuntu 22.04)。分離したサービスの前段に nginx のリバースプロキシを置き、各サービスはシェルを持たない専用ユーザーで、ProtectSystem=strict と NoNewPrivileges を設定して動かしています。
  • 初日の夜(2026年8月23日):管理者パスワードの再生成、SSH への fail2ban、厳格なセキュリティヘッダー(CSP、HSTS、X-Frame-Options)、アクセスログの IP 末尾オクテットのゼロ化、journald の保存期間の制限、nginx ログのローテーション、VM の外に保管する機密情報の暗号化バックアップ。
  • その後:nginx のレート制限、rpcbind の無効化、スワップ、リモートデスクトップを 127.0.0.1 のみに限定(SSH トンネル経由のみ)、SSH のパスワード認証の無効化、Full (strict) モードでの Cloudflare Origin CA 証明書。
  • プル型の継続的デプロイ:master への push を GitHub Actions が検証して prod タグを移し、VM が systemd タイマーでそれを取得して動作確認し、問題があれば自動でロールバックします。受信ポートは開けず、VM の認証情報を GitHub に置くこともありません。
  • 二重のファイアウォール(VM の iptables とクラウドの Security List)。マシンの中からだけでテストしない理由です。
  • 意図的な割り切り:セキュリティ更新は自動でインストールされますが、VM が勝手に再起動したり、メジャーバージョンを上げたりすることはありません。
  • アクセス計測は自前のみ:サードパーティ Cookie は使わず、IP アドレスは保存前にハッシュ化し、記録は12か月で削除します。

設計判断とトレードオフ

  • ブラウザで読み込むのではなく、サーバー側でページを生成:7つの言語すべてで検索エンジンにインデックスされ、JavaScript なしでも読めます。
  • Docker ではなく systemd サービス:本番は CPU とメモリの上限を設けた、サンドボックス化した systemd ユニットで動いています。デプロイされることのないコンテナでは、何も証明できません。
  • 2つ目の API を作るのではなく、公開 JSON フィード(/api/contenus.json)をひとつだけ用意し、Mindmap でも再利用:情報源はひとつなので、同期の手間もありません。

発生した問題と対処

  • サイトのリニューアル後、pull request の自動レポートが、埋め込んだスクリーンショットのせいで GitHub のコメント上限 65,536 文字を超えてしまいました。テストは通っても、投稿に失敗していたのです。サムネイルを軽くし、60,000 文字以内に収めることで解決しました。
  • デプロイ後も、Cloudflare が最大4時間、古いスクリプトを配信し続けていました。いまはすべての CSS と JavaScript の URL に内容のフィンガープリント(?v=)を付けているため、新しいリリースが古いファイルと混ざることはありません。
  • アラビア語のページで、「43 → 9.9 MB」のような値が双方向テキストのアルゴリズムによって逆向きに表示されていました。いまはラテン文字の値を Unicode の制御文字で分離しています。

まだ足りない点

  • 自動再起動なし(意図的な選択)