Site Hardening Web / Security / Infrastructure
← Back to Blog

ウェブサイトを一から見直して、
セキュアな本番サイトに昇格させた話
構造の整理 / フォームの防衛 / シークレット管理 / メール認証 / 検索エンジン登録まで、一気通貫で整える

PROLOGUE

「動いているから大丈夫」は、本番サイトでは通用しない

edencode.jp は、最初はただの名刺代わりの静的ページとして立ち上げた。Home があって、Works があって、連絡先は mailto: でメーラーが開くだけ。動いてはいるけれど、「運用している」と胸を張れる状態ではなかった

これから仕事の依頼も受けるし、記事もどんどん増えていく。そうなった時に困るのは、見た目のクオリティよりも見えないところの作り込みだ。問い合わせフォームに bot が押し寄せたら、スパムメールに埋もれる。メールの送信元認証が無ければ、迷惑メールに振り分けられる。シークレットキーをコードに直書きしていたら、うっかり GitHub に公開した瞬間に乗っ取られる。

この日は「見た目を増やさない日」にした。代わりに、簡易ページから本番品質のサイトへ昇格させるための裏側の工事を一気通貫でやっていく。同じ手順で自分のサイトも固めたい人のために、順を追って書いていく。

PHASE 1

構造を整える — 「一箇所直せば全部に反映される」を作る

セキュリティの前に、まず構造から。理由は単純で、構造が散らかっていると、セキュリティの穴を塞いだつもりでも別のページだけ抜けている、という事故が起きるから。

① 共通スタイルを 1 ファイルに集約する
カラー変数やリセット CSS、アクセシビリティ関連のような「全ページが使うもの」を css/site-common.css にまとめた。各ページでは <link> 1 行読み込むだけ。テーマカラーを変えたい時にこの 1 ファイルを直せば、サイト全体に反映される。

② 記事メタデータをデータファイルに分離する
これまで、トップページのブログカードは index.html の中に直書きしていた。新記事を追加するたびに HTML を触る構造だ。これを js/posts-data.js に切り出し、「配列の先頭に 1 件足すだけで新記事が反映される」形に変えた。

// js/posts-data.js — 新記事を足すのはこの配列の先頭に1件だけ window.EDENCODE_POSTS = [ { date: "2026.04.22", cat: "log", href: "posts/20260422_site_hardening.html", title: "ウェブサイトを一から見直して...", excerpt: "..." }, // ...以下既存記事 ];

構造整理の指針は 「未来の自分を憎まない設計にする」。半年後にデザイン変えたい時、200 ファイル直すか、1 ファイル直すかの差はここで決まる。

PHASE 2

問い合わせフォームに「五重の防衛ライン」を敷く

本番サイトの中で一番攻撃対象になりやすいのがフォームだ。ここを mailto: から PHP の送信フォームに切り替えて、防衛ラインを複数重ねていく。1 つだけ強い壁を作るより、薄い壁を 5 枚重ねるほうが実戦では堅い

① CSRF トークン — フォーム表示時にランダムな使い捨てトークンを発行してセッションに保存し、送信時に一致するかチェックする。これで他サイトから偽装リクエストを投げる攻撃(CSRF)を防げる。

② ハニーポット — 人間には見えない隠しフィールドを用意する。フォームを自動で全部埋めてくる bot はここにも値を入れるので、そこに値が入っていたら即 bot 判定。見えないから人間が間違って埋める心配はない。

<!-- HTMLとしては置くけど、CSSで画面外に飛ばして人間には見せない --> <input type="text" name="website" tabindex="-1" autocomplete="off">

③ 最低滞在時間チェック — フォームを表示した時刻を hidden で送り、受信側で「1 秒未満の送信は bot」として弾く。人間はどんなに早くても 2〜3 秒はかかる。

④ Cloudflare Turnstile — 「このチェックを押してください」の新しい形。画像を選ばせる CAPTCHA と違って、裏側でブラウザの挙動を見て人間かどうかを判定する。ユーザーは何も押さなくていいのが強み。

⑤ IP ベースのレート制限 — 同じ IP から 60 秒に 3 回以上送信されたら拒否する、というのをファイルベースで記録する。DB を用意しなくても動く簡易版。

おまけとして、メールヘッダインジェクション対策(入力値から改行コードを除去)、入力長の上限カテゴリの許可リスト検証もセットで入れる。このへんは「やっていないと普通に狙われる」基本装備。

PHASE 3

シークレットを .env に追い出す

Turnstile を動かすには、シークレットキーを PHP 側に持たせる必要がある。ここで絶対にやってはいけないのがコードに直書きすること。理由は単純で、コードは GitHub に push するから。公開リポジトリでも、プライベートリポジトリでも、共同作業者が増えた瞬間に漏れる

解決策が .env ファイル。シークレットは全部ここに書き、コード側は環境変数として読む。

# .env (Gitには絶対にコミットしない) TURNSTILE_SITE_KEY=0x4AAAAA... TURNSTILE_SECRET_KEY=0x4AAAAA...

PHP 側には軽量ローダを自前で実装した。標準ライブラリで .env を読む仕組みは無いが、file() で 1 行ずつ読んで KEY=VALUEputenv() に流し込むだけで十分動く。

配置場所が重要で、Xサーバーなど一般的な共有サーバーでは、public_html の一つ上のディレクトリに置く/home/<user>/edencode.jp/.env のような位置だ。こうすると Web 経由で URL を叩いても物理的に取得できないし、PHP からは相対パスで読めるので運用に支障はない。

そして .gitignore に以下を追加して、うっかりコミット事故を防ぐ。

# .gitignore .env **/.env !.env.example !**/.env.example

.env.example だけは残してリポジトリに入れておく。「どのキーを設定すればいいか」のサンプルになり、新しい環境にデプロイする時の手順書代わりになる。

PHASE 4

.htaccess で「見せないものは見せない」

万が一、.env や内部メモの .md ファイルを間違ってアップロードしてしまっても、Web 経由で覗けないようにしておく。これは Apache サーバー(Xサーバー含む)なら .htaccess で指定できる。

# .htaccess — public_html 直下に置く # ドットファイル全般を拒否 <FilesMatch "^\."> Require all denied </FilesMatch> # 特定拡張子を拒否(内部メモ/ログ/スクリプト/バックアップ) <FilesMatch "\.(md|log|sh|env|bak)$"> Require all denied </FilesMatch>

これで、https://edencode.jp/.env を叩かれても 404 が返るし、UPGRADE_NOTES.md のような内部用メモも外からは見えない。「置いてはいけないものは置かない」が大原則だけど、二段目の保険として必ず入れておく。

PHASE 5

メールのなりすまし対策 — SPF / DKIM / DMARC 三点セット

独自ドメインでメールを出すなら、絶対にやるのがこの 3 つ。設定していないと、自分が送ったメールが相手の迷惑メールフォルダに直行するし、第三者が「edencode.jp を名乗って」スパムを撒くことも防げない。

SPF(Sender Policy Framework)
DNS に「このドメインから送っていい送信サーバーはこれ」というリストを書いておく。受信側は、メールの送信元サーバーがリストに載っているかを照合する。

DKIM(DomainKeys Identified Mail)
送信時にメール本文へ電子署名を付ける。受信側は DNS から公開鍵を取ってきて、署名を検証する。これで途中で改ざんされていないことが保証できる。

DMARC(Domain-based Message Authentication)
「SPF や DKIM の検証に失敗したメールをどうするか」の方針を DNS に書いておく仕組み。3 段階ある:

  • p=none — 監視のみ(何もしないで、どれが失敗したかだけレポートを集める)
  • p=quarantine — 迷惑メールフォルダに振り分けさせる
  • p=reject — 受信側に拒否させる

ここで一つ罠がある。いきなり p=reject を設定してはいけない。SPF/DKIM の設定が完全でないうちにこれをやると、本物のメールも弾かれて誰にも届かなくなる。

正しい手順は:

  1. まず p=none で 1〜2 週間様子を見る(レポートが届くので、誤判定がないか確認)
  2. 問題なければ p=quarantine に引き上げる
  3. さらに問題なければ最終的に p=reject にする

Xサーバーなら管理画面の「メール認証」から SPF と DKIM をワンクリックで有効化できて、DMARC は DNS の TXT レコードに 1 行追加するだけ。

PHASE 6

Google Search Console に登録して、検索に乗せる

サイトを作っただけでは Google は勝手には見にこない(来ても時間がかかる)。最短で検索に乗せるには、Google Search Console への登録と sitemap.xml の提出が必要になる。

手順はシンプル:

  1. Search Console でドメインプロパティとして edencode.jp を登録(URL プレフィックスではなくドメイン推奨。サブドメインも全部カバーできる)
  2. 所有権確認のために DNS に TXT レコードを追加する(google-site-verification=... の文字列を渡される)
  3. DNS の反映を待ってから「確認」を押す
  4. サイトマップから sitemap.xml の URL を送信する

ここでも 1 つ罠がある。DNS の管理画面で「追加」と「編集」を取り違えないこと。既存の A レコード(サーバーの IP を指している大事なレコード)を編集してしまうと、サイト本体が引き当たらなくなる。必ず「TXT レコードを新規追加」するモードで作業する。

sitemap.xml は、サイト内の全ページの URL をまとめた XML ファイル。手書きでも作れるが、記事が増えてきたら自動生成スクリプトを仕込んでおくと楽。

INSIGHT

見た目は昨日と同じ。でも中身は別物

この日やった作業は、表の画面には何一つ新しく出てこない。記事が増えたわけでも、デザインが変わったわけでもない。増えたのは、壊れにくさ・なりすまされにくさ・運用で事故らない仕組みの方だ。

プロとアマの差って、たぶんここの積み重ねにある。見える機能は誰でも作れる。でも見えない防衛ラインをどこまで引くかは、作り手の思想がそのまま出る。

セキュリティは単独では完結しない。DNS・サーバー・アプリケーション・外部サービス(Cloudflare / Google)それぞれに「身元証明」を積み重ねて、ようやく全体がひとつの信用になる。どれか一つでも抜けていると、他を全部ちゃんとやっていても信頼度はそこに引きずられる。

そして、この工事が終わったからといって「完璧」にはならない。攻撃手法は日々進化するし、サービスのベストプラクティスも更新される。本番サイトは作って終わりじゃなく、育てていくもの。今日はその育て方を覚えた日でもあった。

SUMMARY — 今日やったことチェックリスト

  • 構造整理:共通 CSS を site-common.css に集約 / 記事メタデータを posts-data.js に分離
  • フォーム防衛:CSRF トークン / ハニーポット / 最低滞在時間 / Cloudflare Turnstile / レート制限
  • シークレット管理.env を public_html の外に配置 / .gitignore で除外 / .env.example はサンプルとしてコミット
  • 直アクセス遮断.htaccess でドットファイル・.md.log.sh.env.bak を 404 に
  • メール認証:SPF / DKIM 有効化 / DMARC は p=none から段階運用
  • 検索エンジン登録:Google Search Console にドメインプロパティ登録 / sitemap.xml 送信

見た目は変わらない。でも、ここから先に乗せる記事や作品は、この土台の上で安心して発信できる。土台ができた、という事実そのものがこの日いちばんの成果だった。

ALL AI / Gen Tools Web Artwork