
Linuxサーバーのハードニングとは、インストール直後のサーバーから不要な入口を閉じ、侵入されにくく、かつ侵入の兆候に気づける状態へ整える作業です。グローバルIPを付けたUbuntuサーバーは、公開した直後からSSHへのパスワード総当たりが journalctl に並び始めます。放置しても鍵認証なら破られませんが、パスワード認証のまま弱いパスワードを使っていた機器は、これで実際に乗っ取られています。
対象はUbuntu Server 24.04で、VPSでも自宅のミニPCでも同じ手順が使えます。一般ユーザーとsudoの整備、SSH鍵認証への切り替えとパスワード認証の停止、ufwによる受信制限、fail2ban、unattended-upgradesによる自動更新、時刻同期とログの読み方、サービスの棚卸しまでを順に進め、最後に確認コマンドをチェックリストにまとめています。所要時間は30分程度です。
この記事でわかること
- SSH鍵認証(ed25519)の設定と、Ubuntu 24.04特有のsshd_config.dの優先順位の落とし穴
- ufwで受信を既定拒否にする手順と、Dockerがufwを素通りする理由
- fail2banのjail.localに書く具体的な値と、その値を選ぶ根拠
- unattended-upgradesで「セキュリティ更新だけを自動で当てる」設定と再起動の扱い
- 設定後に何をどう確認すればよいか(コマンドと期待する出力)
最初にやること:一般ユーザーの作成とsudo
rootで直接作業を続けると、誰が何をしたかが残らず、SSHの総当たりの標的にもrootという確定したユーザー名を差し出すことになります。先に日常作業用の一般ユーザーを作り、sudo 経由で管理操作をする形にします。sudoで実行したコマンドは journalctl _COMM=sudo で後から追えます。以下のコマンドと設定に出てくる taro はユーザー名の例、203.0.113.10 はサーバーのIPアドレスの例です。自分の値に置き換えて実行してください。
sudo adduser taro # パスワードはsudo実行時に使うので、長いものを設定
sudo usermod -aG sudo taro
su - taro
sudo -l # (ALL : ALL) ALL と出れば管理操作ができるVPSのイメージによっては、初期ユーザー(ubuntu など)に /etc/sudoers.d/90-cloud-init-users で NOPASSWD:ALL が付いています。鍵が漏れた瞬間にパスワードなしでrootになれる設定なので、自分のユーザーで作業できることを確認した後に、このファイルの該当行を sudo visudo -f /etc/sudoers.d/90-cloud-init-users で消すか、初期ユーザーごと削除してください。rootにパスワードが設定されているイメージなら sudo passwd -l root でロックします。
SSH鍵認証の設定とパスワード認証の無効化
公開鍵認証では、サーバーがランダムな値を送り、クライアントがそれを秘密鍵で署名して返します。秘密鍵はネットワークに流れないため、総当たりも盗聴も成立しません。鍵の種類は ed25519 を使います。RSAのように鍵長を選ぶ余地がなく(署名も鍵も短く固定)、暗号強度の面で現在の推奨に沿っており、対応していないSSHサーバーは執筆時点でほぼ残っていません。
# 手元のPCで鍵ペアを作る。パスフレーズは必ず設定する(秘密鍵ファイルの盗難対策)
ssh-keygen -t ed25519 -a 100 -C "taro@laptop-2026"
# 公開鍵をサーバーの ~/.ssh/authorized_keys に登録(この時点ではまだパスワード認証で入れる)
ssh-copy-id [email protected]
# 鍵でログインできることを確認
ssh [email protected]Windowsの標準OpenSSHクライアントには ssh-copy-id がないので、PowerShellから公開鍵の内容を流し込みます。authorized_keys のパーミッションが緩いとsshdは鍵を無視するため、700 と 600 の指定を省略しないでください。
ssh-keygen -t ed25519 -a 100 -C "taro@laptop-2026"
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh [email protected] "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"鍵で入れたら、サーバー側でパスワード認証を止めます。Ubuntu 24.04の /etc/ssh/sshd_config は先頭で Include /etc/ssh/sshd_config.d/*.conf を読み込み、sshdは同じ設定項目は最初に現れた値を採用します。クラウドイメージには 50-cloud-init.conf に PasswordAuthentication yes が入っていることがあるので、それより辞書順で前に来るファイル名で上書きします。
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers taroKbdInteractiveAuthentication no: PAM経由のキーボード対話認証を止めます。PasswordAuthentication noだけでは、この経路でパスワードを聞かれる構成が残ります(旧名はChallengeResponseAuthentication)。AuthenticationMethods publickey: 認証方式を公開鍵のみに限定する明示的な宣言です。MaxAuthTries 3とLoginGraceTime 30: 1接続あたりの試行回数と、認証を終えるまでの猶予秒数。総当たりのコストを上げつつ、未認証接続がプロセスを占有する時間を短くします。AllowUsers taro: 列挙したユーザー以外はログイン不可。taroの部分は必ず自分のユーザー名にします(例のまま書くと自分も入れなくなります)。Ansibleなど別のユーザーで接続する予定があれば空白区切りで追加します。
sudo sshd -t # 文法チェック。何も出なければOK
sudo sshd -T | grep -Ei '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|authenticationmethods)'
# passwordauthentication no
# kbdinteractiveauthentication no
# permitrootlogin no
# authenticationmethods publickey
sudo systemctl restart sshsshdを再起動しても、既存のSSHセッションは切れません。今つないでいる端末は閉じずに、別のターミナルから新規に鍵でログインできること、そして ssh -o PubkeyAuthentication=no ユーザー名@サーバー が Permission denied (publickey) で拒否されることの両方を確認してから、元のセッションを閉じてください。この順序を守らないと、設定ミスで締め出されたときの復旧手段がVPSのコンソールだけになります。
ポート変更の是非とrootログイン禁止
SSHのポートを22から変えることは、防御としては勧めません。理由は3つあります。インターネット全体をスキャンするボットは全ポートを見るので、2222や10022への総当たりも普通に届くこと。鍵認証にした時点で総当たりは成功しないので、隠す対象がそもそもないこと。そして1024以上のポートで待ち受けると、一般ユーザー権限で乗っ取られたプロセスでも偽のsshdを同じポートに立てられること、です。ログのノイズを減らす目的なら効果はあるので、「ノイズ対策」と割り切って変えるのは構いません。
変える場合の注意として、Ubuntu 24.04のsshはソケット起動(ssh.socket)になっており、待ち受けポートは sshd_config の Port をジェネレーターが読んでソケットに反映します。設定を変えたら sudo systemctl daemon-reload と sudo systemctl restart ssh.socket の両方が必要で、ufwの許可ルールも新しいポートに合わせて追加してから古い22を閉じます。
PermitRootLogin のUbuntu既定値は prohibit-password(鍵ならrootで入れる)です。クラウドイメージではrootに管理用の鍵が仕込まれていることがあるので、自分のユーザーとsudoが動いていることを確かめたうえで no にし、/root/.ssh/authorized_keys の中身も確認しておきます。
ufwでファイアウォールを設定する
ufwはnftables(iptables互換層)のフロントエンドで、「受信は既定で拒否、必要なポートだけ許可」を数行で書けます。有効化の前に必ずSSHを許可してください。順序を逆にすると、その場で接続が切れます。
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH # 22/tcp を許可しつつ、30秒に6回を超える接続元を拒否
sudo ufw allow 80/tcp # Webを公開する場合のみ
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseStatus: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) LIMIT IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) LIMIT IN Anywhere (v6)limit は allow の上位互換で、同一IPから30秒間に6回を超える新規接続を拒否します。鍵認証と組み合わせれば、fail2banを入れる前の段階でも総当たりの大半はここで止まります。管理用の接続元が固定IPなら sudo ufw limit from 203.0.113.0/24 to any port 22 proto tcp のように送信元を絞る方が確実です。
Dockerで -p 8080:80 のようにポートを公開すると、Dockerが自前のnftablesチェーン(DOCKERチェーン)でNATするため、ufwの受信拒否を素通りして外部からアクセスできます。ufw status に出ていないポートが開いている典型的な原因です。公開したくないコンテナは -p 127.0.0.1:8080:80 とループバックに束ね、外部にはリバースプロキシ経由で出してください。
fail2banの導入と設定
仕組みと、鍵認証にしたあとでも入れる理由
fail2banはログ(またはsystemdジャーナル)を監視し、findtime 秒以内に maxretry 回失敗した送信元を bantime 秒だけファイアウォールで遮断します。鍵認証のみにした後の総当たりは成功しませんが、1日に数万行の Invalid user がジャーナルに積もり、本当に見たいログが埋もれます。fail2banの主な効用は「ログのノイズ削減」と「同じ相手にCPUとメモリを使わせない」ことで、パスワード認証が残るサービス(Webのログイン画面、メール)を同居させるなら防御としても意味を持ちます。
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF'
[DEFAULT]
backend = systemd
banaction = nftables-multiport
banaction_allports = nftables-allports
bantime = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
bantime.maxtime = 1w
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
[sshd]
enabled = true
mode = aggressive
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshdbackend = systemd:/var/log/auth.logではなくジャーナルを直接読みます。rsyslogが入っていない最小構成でも動きます。banaction = nftables-multiport: 遮断をnftablesで行います。Ubuntu 24.04はnftablesが標準なので、iptables系のアクションより素直です。bantime.increment = trueとbantime.maxtime = 1w: 再犯のたびに遮断時間を倍増させ、上限を1週間にします。初回1時間にしているのは、社内の誰かが鍵を間違えて閉め出されたときの被害を限定するためです。ignoreip: 自社の固定IPや監視サーバーを除外します。ここを書き忘れて自分を遮断するのがfail2banで最も多い事故です。mode = aggressive: 鍵認証拒否後のConnection closed by authenticating userや、認証前に切断するスキャナーも検知対象に含めます。
Status for the jail: sshd
|- Filter
| |- Currently failed: 3
| |- Total failed: 412
| `- Journal matches: _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
|- Currently banned: 7
|- Total banned: 58
`- Banned IP list: 198.51.100.23 198.51.100.77 ...誤って遮断した相手は sudo fail2ban-client set sshd unbanip 198.51.100.23 で解除できます。遮断中のルールは sudo nft list table inet f2b-table で見えます。
unattended-upgradesで自動更新
Ubuntu Serverでは unattended-upgrades が最初から入っており、既定で「セキュリティ更新(noble-security)だけを毎日自動適用」します。まず既定の状態を確認し、必要なら再起動の扱いと不要パッケージの掃除を追加します。
cat /etc/apt/apt.conf.d/20auto-upgrades
# APT::Periodic::Update-Package-Lists "1";
# APT::Periodic::Unattended-Upgrade "1";
# → 両方 "1" なら有効。なければ sudo dpkg-reconfigure -plow unattended-upgrades で作成
sudo unattended-upgrade --dry-run --debug 2>&1 | tail -n 5 # 何が適用対象かの予行演習Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
// "${distro_id}:${distro_codename}-updates"; ← 通常更新も含めたい場合だけ有効化
};
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:30";自動再起動を有効にするかどうかは、そのサーバーが止まってよいかで決めます。カーネルやlibcの更新は再起動するまで効かず、/var/run/reboot-required が置かれたまま数か月動いているサーバーは珍しくありません。ホームラボや、止まっても翌朝に気づけば済む社内サーバーなら Automatic-Reboot "true" で深夜に再起動させる方が結果的に安全です。冗長化されていない本番サーバーは "false" にし、代わりに週次で reboot-required の有無を監視して、メンテナンス窓で手動再起動します。-updates(セキュリティ以外の通常更新)は挙動が変わる更新も含むため、既定のセキュリティのみで運用し、通常更新は月1回 apt upgrade を目で見ながら当てる方が事故が少ないです。適用結果は /var/log/unattended-upgrades/unattended-upgrades.log に残ります。
時刻同期とログの確認
時刻がずれたサーバーでは、TLS証明書の検証が失敗し、TOTPの認証コードが通らず、インシデント時に複数機器のログを突き合わせられなくなります。Ubuntuは systemd-timesyncd が既定で動いており、参照先を国内のNTPサーバーに変えるだけで十分です。
sudo timedatectl set-timezone Asia/Tokyo
sudo sed -i 's/^#\?NTP=.*/NTP=ntp.nict.jp/' /etc/systemd/timesyncd.conf
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status
# Server: 133.243.238.164 (ntp.nict.jp)
# Poll interval: 1min 4s
# Offset: +1.203ms ← 数ms以内なら正常ログはジャーナルに集約されています。Ubuntuでは /var/log/journal が存在するため再起動をまたいで保持されますが、上限を決めておかないとディスクを圧迫します。/etc/systemd/journald.conf に SystemMaxUse=1G を書いて sudo systemctl restart systemd-journald で反映します。日常的に見るコマンドは次の4つです。
journalctl -u ssh -S today # 今日のSSH認証ログ
# Accepted publickey for taro from 203.0.113.5 port 50212 ssh2: ED25519 SHA256:...
# Invalid user admin from 198.51.100.23 port 40122 ← 総当たり
journalctl _COMM=sudo -S -7d # 1週間分のsudo実行履歴(誰が何を実行したか)
sudo lastb | head # 失敗ログインの一覧
journalctl -p err -b # 今回起動以降のエラー最小権限とサービスの棚卸し
攻撃面はポートの数に比例します。待ち受けているポートを列挙し、「なぜ開いているか」を説明できないものを止めるか、ループバックに束ねます。
sudo ss -tulpn
# Netid State Local Address:Port Process
# tcp LISTEN 0.0.0.0:22 users:(("sshd",...)) ← 必要
# tcp LISTEN 127.0.0.53%lo:53 users:(("systemd-resolve"...)) ← ローカルDNS。問題なし
# tcp LISTEN 0.0.0.0:111 users:(("rpcbind",...)) ← NFSを使わないなら不要
systemctl list-units --type=service --state=running
sudo systemctl disable --now rpcbind.socket rpcbind.service # 不要なら停止と自動起動の解除判断の目安は、0.0.0.0 や [::] で待ち受けているものはすべて「外部から到達できる」と見なすことです。データベースや監視エージェントの管理ポートは、それぞれの設定で 127.0.0.1 に束ねます(MySQL/MariaDBなら bind-address = 127.0.0.1、PostgreSQLなら listen_addresses = 'localhost')。ufwで塞いでいても、設定の二重化が「ufwを一時的に無効にした日」の事故を防ぎます。
ユーザーについても同じ棚卸しをします。awk -F: '$3>=1000 && $1!="nobody" {print $1}' /etc/passwd で一般ユーザーを列挙し、使っていないアカウントは sudo usermod -L -e 1970-01-02 ユーザー名(ロックと期限切れの両方)で無効化します。アプリケーション用のユーザーは --system --shell /usr/sbin/nologin で作り、ログインできないようにしておきます。
確認方法:設定が効いているかを一気に見る
# 1. パスワード認証が本当に拒否されるか(別の端末から)
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]
# [email protected]: Permission denied (publickey). ← これが正解
# 2. sshdの実効設定
sudo sshd -T | grep -Ei '^(passwordauthentication|permitrootlogin) '
# 3. 開いているポートと ufw の状態が一致しているか
sudo ss -tulpn | grep -v 127.0.0 ; sudo ufw status numbered
# 4. fail2ban が sshd を監視しているか
sudo fail2ban-client status sshd | grep -E 'Journal|Currently banned'
# 5. 自動更新が動いた形跡
grep -E 'Packages that will be upgraded|No packages found' /var/log/unattended-upgrades/unattended-upgrades.log | tail -n 3
# 6. 再起動待ちのカーネル更新がないか
cat /var/run/reboot-required 2>/dev/null || echo "reboot not required"つまずきポイント
鍵を登録したのに「Permission denied (publickey)」
サーバー側の journalctl -u ssh に Authentication refused: bad ownership or modes for directory /home/taro/.ssh と出ていれば、パーミッションの問題です。~/.ssh は 700、authorized_keys は 600、所有者は本人でなければsshdは鍵を読みません。ホームディレクトリ自体が 777 になっている場合も同じ理由で拒否されます。ログに何も出ないなら、クライアント側で ssh -v を付け、Offering public key の行に意図した鍵が出ているか、ユーザー名を間違えていないかを見ます。
「PasswordAuthentication no」を書いたのにパスワードで入れてしまう
sshd_config.d/50-cloud-init.conf の PasswordAuthentication yes が先に読まれています。sudo sshd -T | grep -i passwordauth で実効値を確認し、01-hardening.conf のように辞書順で前に来るファイル名にするか、cloud-initのファイルを削除してください。sshd -T は「最終的にどの値が採用されたか」を出すので、設定ファイルを目で追うより確実です。
fail2banが起動しない:「Have not found any log file for sshd jail」
rsyslogが入っておらず /var/log/auth.log が存在しない環境で、既定の auto バックエンドがファイルを探しに行って失敗しています。jail.local の [DEFAULT] に backend = systemd を入れて sudo systemctl restart fail2ban で解決します。設定の文法確認は sudo fail2ban-client -t でできます。
ufwを有効にしたら自分が切断された・Webが見えなくなった
SSH許可の前に ufw enable を実行した、またはWebサーバーのポートを許可し忘れたケースです。VPSのコンソール(VNC/シリアル)から入り sudo ufw allow OpenSSH を追加します。次回に備えて、ufwの変更は sudo ufw --dry-run で確認する癖と、コンソールアクセスの手順をメモしておくことを勧めます。
チェックリスト
| 項目 | 確認コマンド | 期待する状態 |
|---|---|---|
| 一般ユーザーでsudoできる | sudo -l | (ALL : ALL) ALL。NOPASSWDが不要に付いていない |
| パスワード認証の停止 | sudo sshd -T | grep -i passwordauth | passwordauthentication no |
| rootログイン禁止 | sudo sshd -T | grep -i permitroot | permitrootlogin no |
| 受信の既定拒否 | sudo ufw status verbose | Default: deny (incoming)、SSHは LIMIT |
| fail2ban稼働 | sudo fail2ban-client status sshd | jailが表示され、Journal matchesが出る |
| 自動更新 | cat /etc/apt/apt.conf.d/20auto-upgrades | 2行とも "1" |
| 時刻同期 | timedatectl | System clock synchronized: yes |
| 不要な待ち受けがない | sudo ss -tulpn | 0.0.0.0 で待つのは説明できるものだけ |
| 再起動待ちの更新 | cat /var/run/reboot-required | ファイルがない、または計画的に再起動済み |
ここまでの設定は、サーバーを2台目、3台目と増やすたびに手で繰り返すものではありません。同じ手順をAnsibleでコード化しておけば、新しいサーバーを作った日のうちに同じ状態へ揃えられます。公開サーバーでコンテナを動かすなら、OSだけでなくイメージ側の脆弱性もTrivyで定期的にスキャンする必要があります。
よくある質問
SSHのポートは変えるべき?
防御目的では不要です。鍵認証のみにした時点で総当たりは成立せず、ボットは非標準ポートもスキャンします。ログのノイズを減らす目的で変えるのは構いませんが、Ubuntu 24.04では ssh.socket の再起動とufwのルール追加が伴うことと、1024以上のポートは一般ユーザーでも待ち受けられる点を理解したうえで判断してください。
fail2banは必要ですか?
鍵認証のみのSSHだけを公開しているなら、なくても侵入はされません。それでも入れる価値があるのは、ジャーナルが総当たりの記録で埋まるのを防ぎ、本当に見るべきログを読める状態に保てるからです。パスワード認証が残るWebアプリやメールサーバーを同居させる場合は、防御としても必要になります。
自動更新でサービスが止まりませんか?
既定ではセキュリティ更新だけが対象で、パッケージの更新時にそのサービスが再起動されることはあります(数秒程度)。止まると困るのは自動再起動を有効にした場合のOS再起動なので、冗長化のないサーバーでは Automatic-Reboot を false にし、reboot-required を監視して計画的に再起動してください。通常更新(-updates)は自動化せず、目で見て当てる運用を勧めます。
まとめ
効果が大きい順に並べると、SSHの鍵認証化とパスワード認証の停止、ufwでの受信既定拒否、セキュリティ更新の自動適用の3つで、公開サーバーへの侵入経路の大半は塞がります。fail2banと時刻同期、サービスの棚卸しは「気づける状態」を作るための整備です。設定を書いたら必ず sshd -T、ufw status、fail2ban-client status で実効値を確認し、既存セッションを残したまま別端末から検証する。この順序さえ守れば、締め出しの事故は起きません。