暗いデータセンターに置かれた黒いサーバーブレードが、発光するエメラルド色の六角形のシールドに包まれ、飛来する緑色の光の粒子がその障壁にぶつかって砕け散っている
セキュリティガイド

新規VPSを最初の1時間で堅牢化

公開IPv4アドレスは、最初のパケットが届いてから数分のうちに、あなたのことなど知らないし今後も知ることのないマシンによって探査され始めます。これは朗報です。新規サーバーに向けられる攻撃のほとんどは定型的なものであり、意図的に費やす1時間でそのほぼすべてを退けられます。ただし同じ1時間でも、順序を誤れば、誰もあなたの代わりにログインできないマシンから締め出されてしまいます。

支払いの確認からおよそ47秒でrootシェルをお渡しし、そこで意図的に手を止めます。エージェントのインストールも、ファイアウォールの管理も行いませんし、認証情報の控えも保持しません――自分たちが保持する鍵は、開示を強制されうる鍵だからです。この原則はプラットフォーム全体を貫いています。結論はシンプルであり、はっきり言う価値があります。サーバーの安全性は、最初の1時間であなたがどんな状態に残すかで決まるのです。

以下は、私たち自身が実行している順序どおりの、その1時間の内容です。対象はDebian 13とUbuntu 24.04です。二つの設定がほとんどの仕事をこなします。このページの残りが存在するのは、一見正しく見え、どのチュートリアルの最終チェックも通過するのに、ひそかに間違っている三つの落とし穴があるからです。気づかないうちに上書きされるSSHのドロップイン設定、ソケットアクティベーションされたsshdが無視するポート設定、そしてファイアウォールの下からポートを公開してしまうコンテナランタイムです。これらはどれも、それ以外のすべてを正しく行った人を過去に痛い目に遭わせてきました。

実際にドアをノックしているのは何か

1時間オンラインになっているサーバーでjournalctl -u sshを見ると、最初はその量に驚かされます。しかし驚く必要はありません。あなたが目にしているのは、いわばインターネットの背景放射です。学術目的のものも、商用のものも、犯罪目的のものも含む一握りのスキャン活動が、IPv4アドレス空間全体を絶え間なく列挙し、その結果を認証情報推測ボットに渡しているのです。あなたのアドレスに到達したのは、それが番号順に存在しているからにすぎず、あなたが何をしようと数時間後にはまた到達されます。

これは問題の姿を、扱いやすい形に変えてくれます。あなたが対峙しているのは、あなたを狙って選び、状況に応じて適応してくる敵ではなく、決まった手札しか持たないスクリプトです――root、admin、ubuntu、test、git、oracle、postgres、そして情報漏洩で流出したデータに現れた2万個のパスワードです。それには忍耐も創造性もなく、一度で応答しないマシンへの関心もありません。パスワード認証を無効にしても、この攻撃者の動きが遅くなるわけではありません。盤面からまるごと排除されるのです。

知っておく価値のある点が二つあります。第一に、IPv6ははるかに静かです。/64を総当たりで走査することはできないからです――しかし、AAAAレコードが公開されたり、あなたのマシンのアドレスがメールヘッダーや証明書透明性ログに現れたりした瞬間、その静けさは終わります。IPv6を隠れ場所だと考えては絶対にいけません。小さな干し草の山だと考えてください。第二に、IPv4アドレスには過去があります。あなたより前にそのアドレスを使っていたテナントがおり、そのテナントがメールサーバーをずさんに運用していたり、ブロックリストに載るような何かをホストしていたりすれば、その評判が薄れるまであなたが引き継ぐことになります。メールが重要であれば、それを使い始める前に定番のブロックリストでそのアドレスを確認してください――5分の確認作業で、2週間のメール到達率トラブルシューティングを避けられます。

作業の9割をこなす二つの設定

小規模サーバーが実際に侵害される経緯のほぼすべては、次の二つのどちらかから始まります。推測されうるパスワード、または、稼働しているべきではないのに待ち受けていたサービスです。それぞれに対応する対策がPasswordAuthentication noと、デフォルトポリシーがdropであるファイアウォールです。地味ではありますが、二つ合わせて15分で終わり、このページの他のすべての対策を合わせたものよりも価値があります。

その理由は統計的なものというより構造的なものです。どちらもデフォルトで閉じている――つまり、失敗する方向が安全側に倒れています。鍵認証のみのsshdは、どれだけ試行が届こうとも総当たり攻撃を受けません。パスワードを受け入れるコードパスがそもそも存在しないからです。デフォルト拒否のファイアウォールは、まだインストールしていないサービス――3か月後に追加してlocalhostへのバインドを忘れるであろうデータベースも含めて――を守ってくれます。堅牢化チェックリストのそれ以外の項目はすべて、悪いものの列挙にすぎません。無効にすべき個別の項目のリストであり、その網羅性はリストの出来次第でしかありません。

もし一つの節だけ読んでタブを閉じるつもりなら、この節を読み、下記のステップ2から5までを実行してください。それだけでもこの1時間は十分に報われます。残りは本当に役立つものではありますが、あくまで本当に副次的なものです。

SSH設定が今どこにあるのか、そこに潜む落とし穴

Debian 13とUbuntu 24.04では、/etc/ssh/sshd_configの冒頭にInclude /etc/ssh/sshd_config.d/*.confという行があり、クラウドイメージはそのディレクトリ内に、すでにPasswordAuthenticationを設定済みのファイル――通常は50-cloud-init.conf――を同梱しています。メインファイルを編集して自分のディレクティブを末尾に追記するのは自然に感じられますが、その結果できあがる設定は、書いてあるとおりには動作しません。

このルールこそ、sshdについてほぼ全員を驚かせる唯一の事実です。各キーワードについて、最初に得られた値が勝つのです。これは、あなたがこれまで使ってきたほとんどすべての設定マージの仕組みとは正反対です。Includeはメインファイルの冒頭近くにあるため、ドロップインはsshd_config本体より先に読み込まれます――そして複数のドロップインの間ではアルファベット順が決め手になります。10-hardening.confという名前のファイルは50-cloud-init.confに勝ちます。60-hardening.confという名前のファイルはそれに負け、しかも黙って負けます。再起動が成功したと報告してきても、そこからこれを知ることはできません。

だからこそ、書いたばかりのファイルを決して信用してはいけません。デーモン自身に、実際に何を読み込んだのかを尋ねてください。

sshd -t                 # syntax check — silence means valid
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive|allowusers|port) '

sshd -Tは、すべてのインクルード、上書き、デフォルトが解決された後の実効設定を出力します。その出力がpasswordauthentication yesと示しているなら、ディスク上のどのファイルが何を主張していようと、パスワード認証は有効になっています。それ以外のものは検証としてカウントされません。

二つ目の落とし穴も同じ系統のものです。Ubuntu 24.04はソケットアクティベーションを通じてsshdを起動します。ssh.socketが待ち受けポートを所有しており、sshd_config内のPortは完全に無視されます。systemctl is-enabled ssh.socketで確認してください。有効になっていて別のポートを使いたい場合は、sshd_configではなくsystemctl edit ssh.socketListenStream=のオーバーライドで変更してください。sshd_configでは、設定は正しく見えるのに何の効果もありません。

ポート22から移すこと――茶番ではあるが、安上がりな茶番

この件については正直になりましょう。インターネットの側は正直ではないからです。sshdをポート2222や47000に移しても、腕の立つ攻撃者を止めることはまったくできません。1台のホストに対するTCPフルスキャンは数秒で終わり、サービスはバナーで自ら名乗り、あなたを名指しで狙うと決めた相手なら、コーヒーを飲み終える前に見つけ出します。

実際に得られるのは、認証ログをおよそ95%削減できることであり、これには本物の価値があります。ざっと目を通すだけのログと、実際に読むログとの違いを生むからです。埋もれた信号より、見える信号のほうが優れています。何かを監視しているなら、静かなauth.logは異常を可視化する最も安上がりな方法です。

コストは小さいものの実在し、だからこそ私たちはこれを「推奨」ではなく「任意」と呼んでいます。8か月後に戻ってきたとき、そのポート番号を忘れているでしょう。1024番より上の非標準ポートは、原理上、sshdが待ち受けていない瞬間があれば、権限を持たないプロセスに奪われる可能性があります。クライアント側ネットワークの制限的なアウトバウンドファイアウォールは変わったポートをブロックするため、オフィスやホテルから自分のサーバーに接続できないことがときどき起こります。そしてソケットアクティベーションされたシステムでは、前節で述べた正しい場所で変更しなければなりません。ログを静かに保ちたいならやってください。ただし、それをやったからといって安全になったと感じてはいけませんし、鍵認証のみにする代わりにこれをやることは絶対にやめてください。

デフォルト拒否と、私たちが実際に使っているルールセット

何をブロックするかを列挙するファイアウォールは、単なる書類整理です。何を許可するかを列挙するファイアウォールこそが、セキュリティ制御です。この違いがすべてを決めます。来月インストールするサービスも、金曜日に開けたままのデバッグ用ポートも、勝手に自分を公開してしまったコンテナも、カバーできるのは後者だけだからです。

当社のプラットフォームには二つの層があり、それぞれ独立しています。エッジフィルターは任意で、パネルからサーバーごとに設定し、ハイパーバイザー上で強制されるため、ブロックされたトラフィックはそもそもゲストに届きません――カーネルがサイクルを費やす前に適用したいレイヤー4のルールや、作業中に侵害されたゲストへの到達を防ぎたい場合に有用です。ゲストファイアウォールは完全にあなたのものです。nftables、iptables、pfなど、あなたのイメージが同梱しているものを使います。私たちがこれに触れることは一切ありません。マシンが重要であれば両方を使ってください。少なくとも後者は常に使ってください。

ステップ5のルールセットのうち二点は説明が必要です。コピー&ペーストされたルールセットの多くが、ここを間違えているからです。ICMPをすべてdropしないでください。一見すっきりして見えますが、これはパスMTUディスカバリーを壊します。その結果生じるのは最悪の部類のバグです。小さなリクエストは通るのに、大きなレスポンスはハングし、ログのどこにもファイアウォールのことなど出てこないのです。少なくともdestination-unreachabletime-exceededparameter-problemは許可してください。リストを把握していない限り、ICMPv6をタイプ別にフィルタリングしないでください。IPv6は近隣探索とルーター広告をICMPv6に依存しています。広範にブロックすると、IPv6の接続性は、あたかもルーティングの問題であるかのような形で失われます。単一ホスト上ですべてのICMPv6を許可するのは妥当な妥協であり、以下のルールセットもまさにそうしています。

flush rulesetを実行する前に一つ警告があります。Dockerがインストールされている場合、この行はDockerが書き込んだNATおよびfilterルールを削除してしまい、systemctl restart dockerでそれらを復元するまでコンテナのネットワークが機能しなくなります。まずルールセットを読み込み、次にDockerを再起動し、コンテナのポートを一つでも公開する前に、次の節を読んでください。

開いているとは知らなかったポート

マシンに、実際に何を待ち受けているか尋ねてください。インストールしたつもりのものではなく、今この瞬間にバインドされているものです。

ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'

このフィルターを通り抜けて残るものはすべて、マシン自身以外のどこかから到達可能です。標準イメージでよく見つかるのは、111番のrpcbind(あなたが動かしているものは何も必要としません)、ベースインストールの残骸であるexim4のリスナー、そしてループバック上では無害だが、そうでなければオープンリゾルバになってしまうsystemd-resolvedのスタブです。危険なものは後からやって来ます。あなたが意図的にインストールしたソフトウェアと共にです。チュートリアルがlisten_addressesを編集しろと書いていたせいで0.0.0.0:5432で待ち受けるPostgreSQL、その歴史の大半でデフォルトのパスワードがなかったせいでパスワードなしのRedis、Elasticsearchのノード、Prometheusのエクスポーター、Jupyterノートブックなどです。これらのどれもが、実際の侵害の第一歩となった実績を何度も持っています。

ここに、注意深い人ほど引っかかる落とし穴があります。Dockerはあなたのファイアウォールに許可を求めません。-p 5432:5432と書くと、DockerはnatテーブルのPREROUTINGチェーンにDNATルールを挿入します。これはカーネルが、あなたのinputチェーンがパケットを目にする前に評価するものです。そのトラフィックはその後コンテナへ転送されます。あなたのデフォルト拒否のinputポリシーは一切参照されず、ufw statusはそのポートを閉じていると表示し、そしてデータベースは公開インターネット上に存在することになります。これはDockerの仕様として文書化された挙動であり、10年前からこの通りであり、これによって膨大な数のデータベースが露出してきました。

対処法は一言で済み、身につける価値のある習慣です。

# wrong — reachable from the entire internet, whatever your firewall says
docker run -p 5432:5432 postgres

# right — bound to loopback, reachable only through an SSH tunnel or a proxy
docker run -p 127.0.0.1:5432:5432 postgres

最後に、外部の視点を得てください。サーバー自身からのポートスキャンは、カーネルがどう考えているかを教えてくれるにすぎません。別の場所からのスキャンこそが、世界からどう見えているかを教えてくれます。そして重要なのはその数字だけです。マシンからではなく、あなたのノートパソコンからnmap -Pn -p- <your-ip>を実行し、その結果を、あなたが想定していたリストと比較してください。

自動アップデート――有効にするなら本気で

小規模サーバーが侵害される原因となる窓は、ゼロデイではありません。CVEが実際に動くPoCとともに公開されてから、あなたがたまたま次にログインするまでの4週間です。攻撃ツールは新しいリモート脆弱性を数日で取り込みますが、「そのうち」パッチを当てるつもりのマシンは、その間ずっと露出したままです。そして、誠実な運用者なら誰でも、その「その間」が実際にはどれほど長いかを知っています。

自動アップデートへの反対理由は「壊れることへの恐れ」であり、これは真剣に受け止める価値があります――そのうえで、範囲を絞り込む価値もあります。自動化の対象は、ディストリビューションのセキュリティ枠に限定してください。ここではメンテナーが、新しい上流リリースを出すのではなく、パッケージ済みのバージョンに修正をバックポートしています。Debianのopensslに対するセキュリティアップデートは、あなたがすでに動かしているのと同じバージョンにパッチを当てたビルドです。それによって挙動が変わるリスクは、リモートの穴を1か月放置するリスクに比べればはるかに小さなものです。機能アップグレードは、本来そうあるべき通り、手動のままにしておいてください。

人々が省略しがちなのが再起動です。ディスク上でパッチが当たったlibsslは、起動時に古いものをマップ済みのプロセスには何の効果もありませんし、カーネルの更新も、それで実際に起動し直すまでは何の効果もありません。あなたが選んだ時間帯での無人再起動を受け入れるか、あるいはneedrestartをインストールして、削除済みのライブラリを参照したまま動いているサービスを教えてもらうか、どちらかを実行してください――ただし、どちらか一方は必ず実行してください。uptimeが340日を示している状態で「自動アップデートは有効です」と言うのは、心地よい幻想にすぎず、私たちが最もよく目にするものでもあります。

fail2ban、CrowdSec、それとも何もしないか

fail2banはログを読み、あるアドレスからの失敗が繰り返されているのに気づくと、しばらくの間そのアドレスをbanします。CrowdSecも同じことをしたうえで、その判定を参加者のネットワーク全体で共有するため、よそで悪さをしたアドレスが、あなたのところに到達する前にブロックできます。どちらも優れたソフトウェアです。しかしどちらも、鍵認証のみのSSHサーバー上では、多くの人が思っているような働きはしません。

PasswordAuthenticationnoになっていれば、SSHへの総当たり攻撃は成功しえません。「成功しにくい」のではなく、成功するコードパスがそもそも存在しないのです。したがって、5回失敗した後にアドレスをbanしても、何かを防ぐわけではありません。ログの量とごくわずかなCPUを削減するだけです。それは実在するメリットではありますが、セキュリティ上のメリットではありませんし、その代償もタダではありません。誤ったログを監視するように書かれた粗悪なjailは、攻撃者を締め出した以上に、多くの管理者を自分自身のサーバーから締め出してきました。

これらのツールが本当に力を発揮するのは、もう一段上の層――あなたが実際に外部公開しているサービスです。ログインフォーム、WordPressの管理画面、レート制限のあるAPI、SMTP AUTHを使うメールサーバーなどです。これらは実際にパスワードを受け付けますし、実際に総当たり攻撃が可能であり、banリストはまさに正しい対策です。したがって、私たちの結論は狭く具体的です。SSH用のjailは省略してください。CrowdSecかfail2banは、パスワード入力欄を持つものの前に置いてください。そしてどちらを選ぶにせよ、まず自分の管理用アドレスをホワイトリストに入れてください――ignoreipが存在するのは、そうしなければ、良くない理由で記憶に残ることになる、まさにそんな晩のためです。

変化が起きたときにマシン自身に知らせてもらう

予防とは、1時間でできることです。検知とは、その1時間で足りていたかどうかを教えてくれるものです。凝ったものである必要はなく、単一のサーバーであれば、三つの安上がりな仕組みでほとんどの範囲をカバーできます。

ログを保持する。多くのイメージでは、ジャーナルは/run上にあり、再起動すると消えてしまいます。つまり、インシデントの記録は、その直後の再起動で消え去るということです。/var/log/journalを作成することは一行で済む対策であり、この節の中で最も価値の高いものです。

ログインを知らせてもらう。/etc/ssh/sshrcに、セッション開始のたびにloggerを実行する一行を加えても、コストはかからず、sshd自体の雑多な出力とは別に、すっきりとしてgrepしやすい記録が得られます。ただし一つ注意点があります。ユーザーが~/.ssh/rcを持っている場合、sshdは/etc/ssh/sshrcの代わりにそちらを実行するため、まさにカスタムのdotfileを持っていそうなアカウントに限って、システム全体向けのファイルがスキップされてしまいます。迂回されないフックが必要な場合は、代わりにpam_execを使ってください。

初日にファイルシステムがどんな状態だったかを把握しておく。AIDEはバイナリ、ライブラリ、unitファイルのハッシュを記録し、それ以降の変更点を報告します。ここには根本的でありながら見過ごされがちな落とし穴があります。監視対象のマシン自身にデータベースを保存していると、そのマシンを侵害した者がデータベースを再生成でき、以後チェックは永遠に「変更なし」と報告し続けることになります。データベースをそのマシンの外にコピーするか、少なくともそのハッシュを別の場所に記録しておけば、このツールは20分をかける価値のあるものになります。そのまま放置すれば、それは単なる気休めです。

ここには単独で述べる価値のある一般原則があります。信用できないログとは、侵害されたマシン上にあるログのことです。本当に頼りにするつもりのもの――ジャーナル、整合性データベース、バックアップ――は、同じ攻撃者が制御していないどこかに、その控えを持つべきです。別の法域にあるもう一台の小さなサーバーは、これに対する正当な答えであり、5ドル程度のものでも十分です。

これが対処しないもの

ここまで述べてきたことはすべて境界防御の作業であり、施錠された玄関ドアを金庫と取り違えないためにも、その境界線を正確に理解しておく価値があります。

稼働中のマシン上のデータは保護しません。堅牢化されたカーネルも閉じたファイアウォールも、ハイパーバイザーへのアクセス権を持つ者や、マシンが稼働している間のRAMの中身には関係がありません。サーバーの電源が切られているとき、押収されたとき、あるいは廃棄されたときのディスクが懸念であれば、それはディスク暗号化の領分であり、失敗の仕方も異なる別の手順です――LUKSでVPSディスクを暗号化するを参照してください。

あなたを匿名にはしません。サーバーは完璧に堅牢化されていても、WHOISレコード、使い回したSSH鍵、アナリティクスのタグ、ホップなしで自宅から接続するSSHクライアント、あるいは二つの身元を結びつけてしまう証明書を通じて、それでも所有者が誰であるかを漏らすことがあります。Moneroで支払っておきながら自分自身のアドレスからログインすれば、その支払いは無意味になります。この失敗モードには専用のページがあります――匿名性を失わせるミスです。

あなたのアプリケーションを直しはしません。SQLインジェクション、認証なしの管理エンドポイント、バックドア入りの依存パッケージは、あなたのsshd設定など気にも留めません。きちんと運用されているサーバーが侵害される経緯の大半は、あなたが自分で書くか導入するかしたサービスのために、意図的に、わざわざ開けたポートを通ってやって来ます。

バックアップではありません。ランサムウェア、変数が空だったrm -rf、失敗したアップグレードは、すべて同じ結末を迎えます。控えを取り、別の場所に置き、必要になる前に一度はそこから復元しておいてください――別のホストへの移行を乗り切れるものにするのと同じ規律が、最悪の火曜日も乗り切れるものにしてくれます。

これらはどれも、この1時間をやる意味がないという主張ではありません。この1時間が何を買ってくれたのかを正確に把握しておくべきだという主張です。

  1. パスワードではなく鍵でデプロイする

    自分のマシン上で鍵ペアを生成し、公開鍵の方をデプロイフォームに貼り付けてください。イメージがそれをインストールしてくれるので、失うようなrootパスワードはそもそも存在しなくなります。cloud-initと表示されたイメージからデプロイする場合は、最初の1時間の設定をまるごとuser-dataとして渡すこともできます――上限は64KiBで、その場合マシンは最初から堅牢化された状態で立ち上がります。

    # on your laptop, not on the server
    ssh-keygen -t ed25519 -a 100 -C "$(whoami)@laptop-cvh"
    cat ~/.ssh/id_ed25519.pub        # paste this into the deploy form

    ツールチェーンの何かが拒否しない限りed25519を使ってください。拒否される場合は4096ビットのRSAでも構いません。秘密鍵はパスフレーズで保護し、エージェントに読み込ませてください。ノートパソコン上のパスフレーズなしの鍵ファイルは、モニターに書き付けたパスワードと同じです。同じ鍵セットがレスキューモードにも注入されており、これがステップ4を復旧可能にしています。

  2. 何かに触れる前に二つ目のセッションを開く

    これは任意の作業ではありませんし、心配しすぎでもありません。ここから先、あなたは自分が接続を経由しているデーモン自体と、そこへの到達を許しているファイアウォール自体を変更していくことになります。マシンへ戻るための命綱として、一つのセッションを開いたまま何もせずにおき、変更はすべて別のセッションで行ってください。変更が間違っていても、開いたままのセッションはすでに認証済みなので元に戻すことができますが、新しい接続は拒否されてしまいます。

    # terminal A — the rope. Log in and leave it alone.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>
    
    # terminal B — where every command below runs.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>

    それぞれの変更は、作業中のセッションを再接続するのではなく、三つ目の接続を新たに開いてテストしてください。もしその三つ目の接続が失敗しても、動作するシェルはまだ二つ残っており、危機ではなく単なる問題として扱えます。

  3. 実際に使うアカウントを作成する

    SSH経由のrootは、まさに最初の1時間だけ便利なものです。それ以降は、sudoを使える名前付きアカウントのほうが、監査証跡を残せますし、打ち間違えたコマンドがフル権限で実行されてしまう事態からも守ってくれますし、マシン自体を無効化することなく侵害されたアカウントだけを無効化できます。

    adduser --disabled-password --gecos "" deploy
    install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh
    cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
    chown deploy:deploy /home/deploy/.ssh/authorized_keys
    chmod 0600 /home/deploy/.ssh/authorized_keys
    usermod -aG sudo deploy
    
    # a long random password, stored in your manager, used only by sudo
    passwd deploy

    そのパスワードは必ず設定してください。パスワードが一度も設定されていないsudoアカウントはsudoを使えません。root権限でのログインをすでに無効化した後にこれに気づくのは、ファイルはすべて正しいのに自分を締め出してしまう典型的なパターンです。次に進む前に確認してください。新しいターミナルからssh deploy@<server-ip>を実行し、続けてsudo -vを実行します。どちらも成功しなければなりません。

  4. SSHのドロップインを書き、デーモンに何を読み込んだか尋ねる

    前述の順序ルールに従えば、早く並ぶ番号付きのドロップインは、クラウドイメージが何を同梱していようとそれに勝ちます。書いて、構文を検証し、実効設定を読み戻して、そのあとで初めてreloadしてください。

    cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PubkeyAuthentication yes
    AuthenticationMethods publickey
    PermitEmptyPasswords no
    AllowUsers deploy
    MaxAuthTries 3
    LoginGraceTime 20
    X11Forwarding no
    AllowAgentForwarding no
    AllowTcpForwarding no      # remove this line if you use ssh -L / -D tunnels
    ClientAliveInterval 300
    ClientAliveCountMax 2
    EOF
    chmod 0644 /etc/ssh/sshd_config.d/10-hardening.conf
    
    sshd -t                                    # silence means the syntax is valid
    sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers) '
    systemctl reload ssh

    そのsshd -Tの出力は、reloadした後ではなく前に読んでください。permitrootlogin nopasswordauthentication noになっていなければなりません。そうなっていない場合、あなたのファイルは、より早く並ぶ別のファイルによって上書きされています――ls /etc/ssh/sshd_config.d/で確認し、自分のファイルの番号をもっと小さくしてください。ここで三つ目のターミナルを開き、deployとしてログインしてください。それがうまくいったときに初めて、命綱のセッションを閉じてください。

  5. デフォルト拒否のファイアウォールを読み込む

    nftablesは両ディストリビューションに標準で同梱されており、iptablesのルールセットを単一の読みやすいファイルに置き換えます。許可するポートは、実際に動かしているサービスに合わせて調整してください――以下のリストはSSHとWebサーバーのみを前提にしています。

    apt install -y nftables
    cat > /etc/nftables.conf <<'EOF'
    #!/usr/sbin/nft -f
    flush ruleset
    
    table inet filter {
      chain input {
        type filter hook input priority filter; policy drop;
    
        ct state established,related accept
        ct state invalid drop
        iif lo accept
    
        # ICMP must live: dropping it blackholes path-MTU discovery.
        ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept
        ip6 nexthdr icmpv6 accept
    
        tcp dport 22 ct state new accept
        tcp dport { 80, 443 } ct state new accept
    
        limit rate 5/minute burst 5 packets log prefix "nft-drop: " level info
      }
      chain forward { type filter hook forward priority filter; policy drop; }
      chain output  { type filter hook output  priority filter; policy accept; }
    }
    EOF
    
    nft -c -f /etc/nftables.conf              # check before you commit to it
    systemctl enable --now nftables
    nft list ruleset | head -40

    Dockerがインストールされている場合、flush rulesetはDockerのルールも削除してしまいます。そのあとでsystemctl restart dockerを実行し、コンテナがまだ応答することを確認してください。それから、ノートパソコンからnmap -Pn -p- <server-ip>を実行し、開いているポートの一覧が、たった今許可したポートと過不足なく一致していることを確認してください。

  6. 無人セキュリティアップグレードを有効にする

    対象はセキュリティ枠のみとし、再起動の時間帯は先延ばしにし続けるものではなく、自分で選んだものにしてください。利用者にとって静かな時間帯を選び、かつ正時ちょうどは避けて、他の誰かのcronと衝突しないようにしてください。

    apt install -y unattended-upgrades needrestart
    cat > /etc/apt/apt.conf.d/51-cvh-auto <<'EOF'
    // Debian. On Ubuntu, replace the two Origins-Pattern lines with:
    //   "${distro_id}:${distro_codename}-security";
    Unattended-Upgrade::Origins-Pattern {
      "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
      "origin=Debian,codename=${distro_codename},label=Debian-Security";
    };
    Unattended-Upgrade::Automatic-Reboot "true";
    Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
    Unattended-Upgrade::Automatic-Reboot-Time "04:17";
    Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
    APT::Periodic::Update-Package-Lists "1";
    APT::Periodic::Unattended-Upgrade "1";
    EOF
    
    unattended-upgrade --dry-run --debug | tail -25
    systemctl status unattended-upgrades --no-pager

    無人での再起動がどうしても受け入れられない場合は、これを"false"に設定し、代わりにneedrestart -bに、削除済みのライブラリを参照したまま動いているサービスや、新しいカーネルがインストール済みかどうかを報告させ、それに基づいて対処してください――ただし、どちらも行わないという選択肢だけは受け入れられません。

  7. 待ち受けているものを閉じ、外部から確認する

    ループバック以外の何かにバインドされているものを洗い出し、使っていないものを削除し、その結果をこのマシンではない別のマシンから確認してください。

    ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'
    
    # the usual leftovers on a base image
    systemctl disable --now rpcbind.socket rpcbind 2>/dev/null
    apt purge -y exim4-base exim4-config exim4-daemon-light 2>/dev/null
    apt autoremove --purge -y
    
    # what each running unit is allowed to reach
    systemd-analyze security --no-pager | head -20

    動かし続ける必要はあるが誰にも見せる必要のないもの――データベース、キャッシュ、管理パネル、メトリクスエクスポーターなど――については、その設定自体の中で127.0.0.1にバインドし、SSHトンネル経由で到達するようにしてください。ssh -L 5432:127.0.0.1:5432 deploy@<server-ip>のようにです。これは、ポートを開けてそのサービス自身の認証がしっかりしていることを祈るよりも明確に優れており、デフォルトで選ぶべきパターンです。

  8. ベースラインを記録し、警報を有効にする

    これは、何かがおかしくなった日――つまり記憶だけではその経緯を再現できなくなる日――にだけ報われる10分間です。

    # persistent journal, so a reboot stops erasing the evidence
    mkdir -p /var/log/journal
    systemd-tmpfiles --create --prefix /var/log/journal
    systemctl restart systemd-journald
    
    # a clean, greppable line per SSH session
    cat > /etc/ssh/sshrc <<'EOF'
    logger -t ssh-login "user=$USER from=${SSH_CONNECTION%% *} tty=${SSH_TTY:-none}"
    EOF
    
    # file-integrity baseline — then get the database off the machine
    apt install -y aide
    aideinit
    mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
    sha256sum /var/lib/aide/aide.db     # copy this hash somewhere else
    
    last -n 20 ; lastb -n 20            # who got in, and who tried

    最後に、サーバーの外部に四つのことを書き留めてください。ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pubから得られるSSHホスト鍵のフィンガープリント、上記のAIDEのハッシュ、意図的に開けたポート、そして秘密鍵の保管場所です。そのメモがあれば、最悪の朝も、調査ではなくチェックリストで済むものに変わります。

比較

各対策を、実際に得られるものでランク付けする

最初の1時間に行う各対策を、その価値を決める二つの問いにかけます。実際に何を止めるのか、そして――チュートリアルが何をほのめかそうと――何を止めないのか、という問いです。
対策何を止めるか何を止めないかコスト評価
鍵認証のみのSSH(PasswordAuthentication no)インターネット上のあらゆる認証情報推測ボットを、構造上恒久的にあなたの秘密鍵を握る者、あるいはsshd自体の欠陥5分、一度きり交渉の余地なし
デフォルト拒否のファイアウォール(nftables)待ち受けていることを忘れていたサービス、そして後から誤ってインストールするすべてのサービス意図的に開けたポートへの攻撃10分、一度きり交渉の余地なし
無人セキュリティアップグレードn-day window――公開されたエクスプロイトから次にログインするまでの数週間ゼロデイ、そしてスケジュールすることのない再起動を必要とするあらゆるもの5分に加えて再起動の時間枠交渉の余地なし
rootの代わりにsudoを使う名前付きユーザーフル権限で実行される打ち間違えたコマンド、そして理由もなくrootとして動くプロセスすでに侵入済みの者の手によるローカル権限昇格3分やる価値あり
ファイル整合性のベースライン(AIDE)システムのバイナリ、ライブラリ、unitファイルへの静かな改変データベースが監視対象のマシン上に残っていれば、あらゆるもの20分に加えて機外へのストレージそのマシンが重要ならやる価値あり
SSHをポート22から移すこと認証ログの量のおよそ95%ポートスキャンを実行する者――つまり重要な相手は全員2分に加えて、ポート番号を忘れる晩任意、ログを静かに保ちたいなら
sshd上のfail2ban / CrowdSec常習犯、そして彼らがあなたに無駄遣いさせるCPUパスワード認証が無効になっていれば何も止めない10分に加えて、本物のロックアウトのリスクSSHでは省略し、自分のアプリで使う
FAQ

よくある質問

デプロイ後、どれくらいでスキャンが始まりますか。

典型的には数分です。インターネット全体を対象とするスキャナーがIPv4アドレス空間全体を絶え間なく走査しているため、新しいアドレスは、そこで何が動いていようと次の周回で到達されます。マシンが最初のパケットに応答した瞬間からプローブされていると考えてください――だからこそ、堅牢化は最初の週末にではなく、最初の1時間のうちに行うのです。

パスワードログインを無効にしていても、fail2banはまだ必要ですか。

SSHに関しては不要です。PasswordAuthentication noにしていれば、総当たり攻撃が勝てるコードパスはそもそも存在しないため、5回失敗した後にbanしても何も防げません――ログのノイズを減らせるという意味では価値がありますが、セキュリティ上の価値ではありません。これらのツールが本来の居場所を見つけるのは、実際にパスワードを受け付けるものの前です。Webのログインフォーム、管理パネル、SMTP AUTHなどです。導入するなら、まず自分自身のアドレスをホワイトリストに入れてください。

ufw、firewalld、それともnftablesか。

デフォルトポリシーがdropになっていて、自分が何を導入したのかを理解している限り、どれでも構いません。ufwは最も親しみやすく、現行のDebianとUbuntuでは内部的にnftablesのルールを書き込みます。素のnftablesは、diffやバージョン管理ができる読みやすい単一のファイルであり、それが私たちがこれを使う理由です。どれを選ぶかは、デフォルトポリシーほど重要ではありません――そして、この三つのどれを使おうと、Dockerがそのすべての下からポートを公開してしまうという事実は変わりません。

SSHをポート22から移すべきですか。

ログを静かにする目的でのみ、そうする価値があります。腕の立つ攻撃者は止まりません――フルスキャンは数秒で終わり、バナーがそのサービスを特定してしまいます。しかし、認証ログからノイズの大半を取り除いてくれるため、本物の異常が見えるようになります。これはログ衛生の話であって、対策として扱ってはいけません。また、変更する前に、自分のsshdがソケットアクティベーションされているかどうかを確認してください。Ubuntu 24.04ではsshd_config内のPortディレクティブは無視され、ポートはssh.socketのものになります。

自動アップデートは、朝の4時に自分のサイトを壊してしまいませんか。

セキュリティ枠に限定していれば、そうなることは非常にまれです。それらのパッケージは、新しい上流リリースではなく、あなたがすでに動かしているバージョンにバックポートされた修正です。現実的なリスクはパッチそのものではなく再起動です――だから再起動の時間帯は自分で選び、Automatic-Reboot-WithUsersをfalseに設定して誰かがログインしている間は待つようにし、サービスが本当に無人で再起動できないのであれば、needrestart -bを定期的に実行してその報告に基づいて対処してください。唯一擁護できない立場は、自動アップデートを有効にしたまま稼働時間が年単位になっていることです。

自分自身を締め出してしまいました。どんな選択肢がありますか。

パネルからサーバーをレスキューモードで起動してください。これは、あなたのアカウントと同じSSH鍵を持つAlpine環境をRAM上に立ち上げ、ディスクは/dev/vdaとしてアンマウントされた状態にします。そのためマウントして、sshdのドロップインやファイアウォールのファイルを修正し、アンマウントして再起動できます。レスキュー起動自体がディスク上の何かに触れることはありません。レスキュー環境のホスト鍵フィンガープリントは普段のものとは異なりますが、これは想定どおりであり、傍受ではありません。

これの代わりにLynisやCISの堅牢化スクリプトを実行するのは良い考えですか。

監査としてなら良い考えですが、代替としては良くありません。Lynisは本当に有用なセカンドオピニオンであり、このページが触れていない事柄も見つけてくれます。CISの自動修復スクリプトはまったく別の話です。企業のワークステーション群向けに設計された数百もの変更を適用してしまい、そのうちのいくつかは、何週間も後になって原因を追いにくい形でサーバーを壊します。まずはこの1時間を手作業でこなし、自分のマシンが何をしているのかを理解してください。そのうえで監査ツールを実行し、その指摘を一つずつ読んでいってください。

堅牢化すれば、自分のサーバーは匿名になりますか。

なりません。この二つを混同するのは、よくある、しかも高くつく間違いです。堅牢化が制御するのは誰が入り込めるかであり、匿名性が制御するのは誰がそれをあなたのものだと知りうるかです。完璧に施錠されたサーバーであっても、WHOISレコード、使い回したSSH鍵、アナリティクスのタグ、自宅のアドレスからの管理者ログインを通じて、所有者の情報は漏れ続けます。Moneroで支払っておきながら自分自身の回線から直接接続すれば、その支払いは完全に無意味になります――これについては匿名性を失わせるミスで扱っています。

Deploy your offshore server.

リージョンを選ぶ。プランを選ぶ。鍵を貼り付ける。支払う。次の47秒は当社が担います。