久しぶりのブログ更新です。
最近、公開サーバーが乗っ取られた、認証情報が漏れた、プラグインの脆弱性を突かれた……という話を本当によく目にします。攻撃側も AI で自動化されてきているようで、「個人サーバーだから狙われない」はもう通用しなさそうです。
このブログが載っているサーバーも含め、私はクラウド(OCI)上にインターネットへ公開しているサーバーを2台持っています。
- Web サーバー:このブログ(WordPress)、Redmine、自作の小さな Web アプリなど
- トレード用サーバー:FX・暗号資産の自動売買 bot
半年ほど前に一度、ファイアウォールの整理や fail2ban の導入はしたのですが、それきりです。さすがに不安になってきたので、普段から開発に使っている Claude Code に一通り点検させてみました。その記録です。
※ 記事の性質上、IP アドレス・ファイル名・ユーザー名・鍵やトークンの情報など、攻撃のヒントになりそうな部分はぼかしています。また、紹介するのは対応が終わった指摘だけです(検討中のものは伏せています)。
頼み方:まずは「見るだけ」
いきなり AI に本番サーバーを触らせるのは怖いので、最初の依頼ははっきり 読み取り専用 にしました。Redmine にチケットを1枚切って、そこにこう書いています。
- やること:両サーバーの読み取り専用の点検(待ち受けポート、sshd 設定、ファイアウォール/fail2ban、更新状況、サービス、ファイル権限、ログ)、外部からの見え方の確認、改善案を優先度つきで一覧に
- やらないこと:設定変更・再起動・パッケージ更新(直すかどうかは私が判断して、別チケットにする)
- 完了条件:点検結果と改善案(重大度・対応案・影響)をチケットに書いて報告
Claude Code は自宅の常駐マシンから SSH で両サーバーに入り、設定ファイルやログ、パッケージの状態を順に読んでいきます。外からの見え方(どのポートが開いているか、どの URL が見えてしまうか)も別途確認していました。
結果がチケットに書き込まれるまで、だいたい7分。
点検結果
まず結論から。
両サーバーとも、乗っ取られた痕跡は見つからなかった(SSH の成功ログインは全期間すべて自宅から)。 ただし Web サーバーには 「Web が1か所破られると、サーバー丸ごと取られる」構造がある。トレード用サーバーは比較的きれい。
ひとまずホッとしつつ、後半の一文にヒヤッとしました。指摘は優先度(高・中・低)つきで十数件。このうち、すでに対応が終わったものは次のとおりです。
| 優先度 | 対象 | 指摘(要約) |
|---|---|---|
| 高 | Web | Web サーバーの権限から root まで上がれる経路がある |
| 高 | Web | Redmine がサポート切れの版のまま(セキュリティ修正が未適用) |
| 高 | Web | 誰でも読めるファイルに認証トークンが平文で書かれている |
| 中 | トレード | 稼働145日、カーネル更新が再起動待ちのまま |
| 中 | Web | 使っていないサービスが外向きに待ち受けている |
| 中 | Web | WordPress まわり(デバッグログが誰でもダウンロード可、管理者ユーザー名が外から分かる、ログイン試行制限なし、更新待ちプラグイン) |
| 中 | Web | fail2ban が SSH しか見ていない(メール認証などは総当たりし放題) |
| 低 | 両方 | sshd で使っていない機能・古い暗号方式が有効 |
| 低 | Web | Apache/PHP のバージョン表示、ディレクトリ一覧が見える、HSTS なし |
| 低 | Web | メールサーバー設定に昔の自宅 LAN の設定が残っている |
「問題なかった点」もちゃんと列挙してくれていて、パスワード認証無効・root ログイン禁止、DB はローカルのみ、設定ファイルの権限、TLS 1.2/1.3 のみ、証明書の自動更新、自動セキュリティ更新、WordPress 本体は最新……などは合格でした。半年前にやったことはちゃんと効いていたようです。
一番ヒヤッとしたもの:Web 権限から root へ
これは自分では絶対に気づかなかったと思います。
Redmine のインストール先が Web サーバーのユーザー(www-data)の持ち物になっていて、その一方で、root で毎朝動く定期ジョブと、sudo で許可していたコマンドが、そのディレクトリの中身を root として実行していました。
つまり、WordPress のプラグインの脆弱性などで www-data の権限を取られると、そのディレクトリに細工 → 翌朝 root で実行される → サーバー丸ごと、という道筋ができていたわけです。どれも「動かすために」その場その場で設定したもので、組み合わさった結果こうなっていたというのが怖いところです。
芋づる式に出てきたもの:平文トークン
「誰でも読めるスクリプトに GitHub のトークンが直書きされている」という指摘を掘っていくと、いろいろ出てきました。
- そのスクリプトを動かすはずの定期ジョブは、ログの書き込み先が無くて 約半年前から一度も動いていなかった
- そのトークン自体は失効済みだったが、別の有効なトークンが、Redmine が参照している Git リポジトリの設定ファイルに埋め込まれていて、そのファイルも誰でも読める状態だった
- GitHub から Redmine へのコミット通知(webhook)は、最近ずっとエラーを返していた。原因はコミットメッセージの絵文字で DB への書き込みが失敗していたこと(セキュリティとは無関係ですが、ついでに見つかった)
「点検」のつもりが、半年放置していた不具合の発見にもなりました。
直した:判断は人間、作業は AI
報告を読んで、優先度の高いものから順に「これは直して」と指示しました。1件ごとに Redmine のチケットを切り、やること/やらないこと/完了条件を書いてから作業させています。
| 指摘 | 対応 |
|---|---|
| Web 権限→root の経路 | アプリ本体を root 所有にし、Web 側が書けるのはデータ置き場だけに。定期ジョブは Web 権限で実行、sudo の許可を削除 |
| 平文トークン | 止まっていた定期ジョブとスクリプトを撤去。リポジトリごとに読み取り専用のデプロイキーを作って差し替え、不要になったトークンはすべて削除 |
| カーネル更新待ち | bot への影響が小さい時間帯にパッケージ更新+再起動 |
| Redmine が古い | まず 5.1 系の最新に上げて穴を塞ぎ、そのあと 7.0 系へ移行 |
| WordPress まわり | デバッグログ削除・無効化、プラグイン更新、ユーザー名が外から分からないように |
| fail2ban | メール認証・WordPress ログインの監視を追加、何度も来る相手は長期 BAN |
| その他(中・低) | 不要サービス停止、sshd の不要機能と古い暗号方式を無効化、バージョン表示を止める、ディレクトリ一覧を無効化、HSTS 追加、HTTP→HTTPS 転送、メール設定の残骸を削除 |
いくつか、やってみて分かったことを書いておきます。
AI でも「人間がやるしかない」部分は残る
GitHub のトークン削除や Redmine の管理画面での設定変更は、Claude Code ではなく私が操作しました。Claude Code は「どの画面で何をすればよいか」と「消す前に他で使われていないか」の確認までやって、ボタンを押すのは私、という分担です。
また、一部の設定変更は Claude Code 自身の安全判定で止められて実行されませんでした。慎重すぎると思う場面もありましたが、本番サーバーを触らせる以上はこのくらいでちょうどいいのかもしれません。
Redmine 7.0 への移行は、別の AI と隔離環境で
メジャーバージョンを2つ飛ばす移行は不安だったので、まず本番のコピーを隔離環境に作って移行と巻き戻しを試すところからやらせました(こちらは OpenAI の Codex に担当させています)。
その結果、Redmine 本体の移行は問題ないものの、
- 使っているチェックリストのプラグインが新しい版でそのままでは動かない
- アプリサーバー(Passenger)の古い版に、新しい Rails と組み合わせると Cookie の扱いを誤る不具合がある
ことが本番を触る前に分かりました。互換修正と更新を入れて再検証してから本番切り替え。1回目の切り替えはファイル権限の問題で途中で止まりましたが、手順どおり旧版に戻して、直してから2回目で成功しています。途中で Codex が利用上限に達して止まり、Claude Code が引き継いで最終確認をする、というリレーもありました。
やらかしも1件
WordPress に小さなプラグインを1つ置く作業で、Claude Code が SSH 越しにファイルを書いた際に引用符が抜けてしまい、ブログ全体が約1分間エラー(500)になりました。すぐに外して復旧し、「手元でファイルを作る → 構文チェック → 配置」の順でやり直しています。
AI に限らず人間でもやる類のミスですが、「本番に置くファイルは手元で作って検査してから」というのは改めて教訓として残しました。
やってみての感想
- 点検そのものは驚くほど速い。 2台分を読み取り専用で一通り見て、優先度つきの報告が出るまで数分。自分でやったら丸1日はかかるし、たぶん権限の組み合わせの問題には気づかない
- 指摘の質が高い。 「なぜ危ないか」「どう直すか」「直すと何が困るか(自分が締め出されるリスクなど)」までセットで出てくる。直すかどうかを判断する材料がそろっている
- ついでに不具合も見つかる。 半年止まっていた定期ジョブ、ずっとエラーになっていた webhook など
- 任せ方が大事。 最初は読み取り専用、変更は1件ずつ承認してから、作業前にバックアップ、作業後は外からの見え方で確認。この型にはめておけば、本番サーバーでも安心して任せられる
朝に頼んで、夕方には中・低の改善と Redmine のメジャーアップデートまで終わっていました。
個人でサーバーを公開している方は、一度「読み取り専用で点検して、優先度つきで改善案を出して」と AI に頼んでみることをおすすめします。少なくとも私は、もっと早くやっておけばよかったと思いました。
おまけ:今回の点検項目(自分用チェックリスト)
- [ ] 待ち受けポートと、外から実際に見えるポート
- [ ] SSH:パスワード認証・root ログイン・不要な転送機能・古い暗号方式
- [ ] ファイアウォールと fail2ban(SSH 以外の入口も見ているか)
- [ ] OS/アプリ/プラグインの更新状況、再起動待ちのカーネル
- [ ] Web の実行ユーザーが書けるファイルを、root が実行していないか
- [ ] 誰でも読めるファイルに、パスワードやトークンが書かれていないか
- [ ] 公開ディレクトリに、ログ・バックアップ・残骸ファイルが置かれていないか
- [ ] ユーザー名やバージョンなど、外に出さなくていい情報が出ていないか
- [ ] 使っていないユーザー・鍵・トークンが残っていないか
- [ ] 定期ジョブが、実際に動いているか