Raspberry Pi 5を堅牢化する。SSH鍵認証・UFW・fail2banと、再起動でIPv6が復活する罠
セットアップ直後のRaspberry Pi 5は、パスワード認証が有効なまま、ファイアウォールなし、更新可能なパッケージ172件と無防備です。SSHの鍵認証化・UFW・fail2ban・自動更新・IPv6無効化を実コマンドと実測検収で解説します。sysctlのIPv6無効化が再起動で復活する現象と恒久対策も実機で確認しました。
こんにちは!
Qualitegプロダクト開発部です!
前回の記事で、モニターなしのRaspberry Pi 5をWindows PCだけでセットアップし、SDカード挿入から約3分でSSH接続まで到達しました。
このラズパイ、ゆくゆくは小さなWebサーバーを載せてインターネット側から使えるようにする予定です。
ただ、その前にやることがあります。
セットアップ直後のRaspberry Pi OSは、実測してみると思った以上に無防備
でした。
前回の検証用セットアップで設定した簡易なパスワードでパスワード認証が有効なまま、ファイアウォールなし、更新可能なパッケージが172件たまり、そして意図していないのにIPv6のグローバルアドレスまで付いていました。
この記事では、
SSHの鍵認証化・UFW・fail2ban・更新の自動適用・IPv6無効化までを実コマンドでやり切り、実際に再起動して設定が生き残ることまで検収します
先にことわっておくと、この検収で1回つまずきました。
sysctlで無効化したはずのIPv6が、再起動したら復活していたのです。
原因はNetworkManagerの接続プロファイルでした。
この顛末は後半で詳しく書きます。
この記事の対象はRaspberry Pi OS(64-bit・Debian 13 trixieベース)です。
作業はすべてWindows PCからSSH越しに行い、ラズパイにモニターは最後まで繋ぎません。記事中の数値は2026年8月14日時点の実測値です。

初期状態を実測したら、ここまで無防備だった
手を入れる前に、まず現状を実測しました。「たぶん危ない」ではなく、どこがどう開いているかを確かめてから塞ぎます。
| 観点 | 初期状態(実測) | 何がまずいか |
|---|---|---|
| SSH認証 | パスワード認証が有効(パスワードは前回設定した検証用の簡易なもの) | 総当たり攻撃の対象になる |
| rootログイン | permitrootlogin without-password | root直ログインの余地が残る |
| ファイアウォール | なし(ufw未導入・nftルール0件) | 全ポート素通し |
| ブルートフォース対策 | なし(fail2ban未導入) | 何回でも試行され放題 |
| IPv6 | グローバルアドレスを保持・[::]:22 で待受 | 運用対象にしていない経路が残っている |
| 更新 | 更新可能なパッケージ172件・自動更新なし | 既知の修正が当たらないまま残る |
| 待受ポート | 22・111(rpcbind)がすべて0.0.0.0で待受(このほか検証用に立てたWebサーバーの8080も稼働中) | 使っていないサービスまで露出 |
IPv6は意外に思うかもしれません。LAN内運用のつもりでも、ルーター広告(RA)を受けて 2409: で始まるグローバルIPv6アドレスが自動で付いていました。
IPv6のグローバルアドレスはNATを前提としないため、IPv4とは露出の考え方が異なります。
本記事ではIPv6を運用対象にしない方針なので、使わない経路を残さないために明示的に無効化します。
方針。ポートは開けない、公開はトンネルで
堅牢化の方針はこう決めました。
SSHは鍵認証のみにして、接続元も自宅LANに限定します。それ以外の受信ポートは、Webサーバー用の8080を含めて一切開けません。
「外から使えるようにしたいのにポートを開けない」のは矛盾に見えますが、公開は次回、WireCanalのトンネル経由で行います。トンネル方式ならラズパイ側は外向きの接続だけで済むので、受信ポートを開ける必要も、ルーターのポート開放も要りません。
つまり今回の堅牢化は「全部閉じる」、
次回の公開は「閉じたまま外に出す」という分担です。
手順1: SSH鍵を仕込む(締め出さない順序がすべて)
最初に鍵を仕込みます。ここで鉄則がひとつあります。鍵ログインの成功を実測してから、パスワード認証を止める。順序を誤ると、自分がSSHで入れなくなります。モニターなし運用のラズパイでは、ヘッドレスのままの復旧がかなり面倒になるので、この順序だけは崩せません。
SSH鍵をまだ持っていない場合は、先にWindows側で ssh-keygen -t ed25519 を実行して作っておきます。
途中で訊かれるパスフレーズは設定しておくのがおすすめです(鍵ファイルが漏れたときの保険になります)。公開鍵は %USERPROFILE%\.ssh\id_ed25519.pub にできます。
まず、ラズパイ側に公開鍵を置きます。
# ラズパイ側(piユーザーで実行)
D=/home/pi/.ssh
mkdir -p "$D"; chmod 700 "$D"
cat > "$D/authorized_keys" <<'EOF'
ssh-ed25519 AAAA...(あなたの公開鍵)
EOF
chmod 600 "$D/authorized_keys"; chown -R pi:pi "$D"なお、cat > は既存の authorized_keys を丸ごと上書きします。今回は新規構築直後なのでこの書き方にしていますが、既に鍵を登録してある環境では追記(>>)にしてください。
GitHubを使っている方は、https://github.com/<アカウント名>.keys で自分の公開鍵一覧を取得できるので、そこからコピーするのが手早いです。
置いたら、Windows側から鍵だけでログインできることを確認します。
ssh -o PasswordAuthentication=no pi@192.168.12.17これが通ってから次へ進みます。
手順2: パスワード変更・IPv6無効化・SSHを鍵のみにする
ここからはroot権限での作業です(sudo -i で入るか、各コマンドに sudo を付けてください)。まずパスワードの変更です。前回の検証用の簡易なパスワードから、きちんとしたものに変えます。鍵のみ化した後もsudoでパスワードは使うので、ここで整えておきます。
passwd pi次に、OS側でIPv6を無効化します。
cat > /etc/sysctl.d/99-disable-ipv6.conf <<'EOF'
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF
sysctl --systemそしてSSHを固めます。設定は本体ファイルを書き換えず、drop-inで追加します。
cat > /etc/ssh/sshd_config.d/00-hardening.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
AddressFamily inet
EOF
sshd -t && systemctl restart sshAddressFamily inet はIPv4のみで待ち受ける指定です。UbuntuだとsshdがソケットActivation(ssh.socket)経由のことがあり、socket側にもdrop-inが要るのですが、今回の実機では ssh は service 直起動(ssh.socket は disabled)だったので、sshd_config の指定だけで [::]:22 が消えました(実測)。
ここで一度、別のターミナルからパスワード認証を試しておきます。拒否されれば成功です。既存の鍵ログインが生きていることも忘れずに確認します。

手順3: 保留172件の更新と、fail2ban・自動更新
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get -y -q full-upgrade # 保留172件を全適用
apt-get -y -q install ufw fail2ban unattended-upgradesfail2banはSSHへの試行失敗を数えて自動でBANしてくれる番人です。ひとつ押さえておきたいのは、今回のRaspberry Pi OS trixie実機にはrsyslogが入っておらず /var/log/auth.log も存在しないことです。
sshdのログはjournaldから読むことになります。Debianのfail2banパッケージにはsshd向けにjournaldを見る設定(sshd_backend = systemd)が入っていますが、古い解説記事はauth.logを前提にしていることが多いので、今回はjail設定でもbackendを明示しました。
cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1h
EOF
systemctl enable --now fail2ban更新の自動適用(unattended-upgrades)も入れます。
夜中に勝手に再起動されると困るので、自動再起動だけは切っておきます。なお定期実行の実体はAPT側のタイマー(apt-daily.timer / apt-daily-upgrade.timer)なので、検収ではタイマーが有効なことまで確認します。
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
cat > /etc/apt/apt.conf.d/52unattended-upgrades-local <<'EOF'
Unattended-Upgrade::Automatic-Reboot "false";
EOF
systemctl enable --now unattended-upgradesひとつ、full-upgradeの副作用で面白い発見がありました。
今回の実機では、初期状態に /etc/sudoers.d/010_pi-nopasswd があってsudoがパスワードなしで通っていたのですが、
full-upgrade後にこのファイルが消えて、sudoがパスワード必須に変わりました
(今回の環境での実測で、一般の仕様変更として確認したものではありません)。堅牢化としては望ましい方向なので、そのまま採用しています。
手順4: UFWは「許可を先に入れてから」有効化する
ファイアウォールで怖いのは自分の締め出しです。UFWはインストール直後は無効(disabled)なので、既定ポリシーを設定した瞬間に遮断が始まるわけではありません。
危険なのは、SSHの許可ルールを入れないまま ufw enable することです。
だから許可ルールを先に入れてから有効化します。
ufw default deny incoming
ufw default allow outgoing
ufw allow from 192.168.12.0/24 to any port 22 proto tcp # 自宅LANのセグメントに合わせる
ufw --force enable新規の受信は原則拒否し、ユーザー定義の許可ルールは自宅LANからのSSH(22)だけにします。検証用に動かしているWebサーバーの8080も、あえて許可しません(次回、ここをトンネルで公開します)。有効化したら、いまのSSHセッションは閉じずに、別のターミナルから新しくSSH接続できることを確認してください。
検収。全項目を実測で通す
設定を書き終わったら検収です。「設定ファイルにそう書いたから大丈夫」ではなく、実際の挙動を外形から確かめます。
| 検収項目 | 実測結果 |
|---|---|
| 鍵ログイン | 成功 |
| パスワード認証 | 拒否(Permission denied (publickey)) |
| sshd実効設定(sshd -T) | passwordauthentication no / permitrootlogin no / addressfamily inet |
| [::]:22 の待受 | 消滅(ss -tlnH で確認) |
| IPv6グローバルアドレス | 0件(ip -6 addr show scope global が空) |
| UFW | active・deny incoming・22/tcpのLAN限定許可のみ |
| ポート外形 | 開発機からTCP22=到達・TCP8080=遮断 |
| fail2ban | sshd jail稼働(journald監視) |
| 自動更新 | apt-daily.timer / apt-daily-upgrade.timer とも enabled・次回実行が予定されている・Automatic-Reboot false |
| 更新可能パッケージ | 172件 → 0件 |

ここまでは順調でした。問題は次です。
再起動したら、IPv6が復活していた
設定の永続性は、実際に再起動しないと分かりません。1回目の再起動検収では、再起動をかけてから約21秒でSSHが復帰し、UFWもfail2banも自動起動していました。ところが。
ip -6 addr show scope global
# → 2409:xx:xxxx:... が、いる消したはずのIPv6グローバルアドレスが、復活していました。
原因はNetworkManagerでした。接続プロファイル側でIPv6が有効なままだったため、sysctlの disable_ipv6 を適用していても、
Wi-Fi接続が確立するタイミングでインターフェース単位の disable_ipv6 が0に書き戻されていました(今回の環境での観測)。
適用直後の確認では ip -6 addr が空になるので成功に見える。しかし再起動してWi-Fiが繋ぎ直されると、設定が巻き戻る。この動きは1回目の再起動検収で初めて表面化しました。
恒久化するには、NetworkManagerの接続プロファイル側でIPv6を無効にします。
# プロファイル名を確認(今回の実機では preconfigured という名前でした)
nmcli connection show --active
# プロファイル側でIPv6を無効化
nmcli connection modify "<上で確認した接続名>" ipv6.method "disabled"これを入れて2回目の再起動検収を実施し、今度は再起動後もグローバルアドレス0件を維持できました。
この件の教訓は、IPv6の設定方法そのものより、こちらだと思っています。
「設定を入れて、その場で確認した」だけでは、永続性は保証されない。再起動まで含めて検収して、初めて完了になる。
検収で1項目でも落ちたら全体を未完として扱い、直してから再検収する。この運用にしていたおかげで、「無効化したつもりの設定が、再起動後には元に戻っていた」状態を公開前に見つけられました。
今回のtrixie実機で確認した、古い手順との違い
Debian 13 trixieベースになって、昔からある手順の前提と違っていた箇所がいくつかありました。
今回の実機で確認できた範囲で整理しておきます。
| 古い前提 | 今回のtrixie実機での実際 |
|---|---|
| fail2banは /var/log/auth.log を読む | rsyslogが無くauth.logも存在しない。sshdのログはjournaldから読む(Debianパッケージ側で sshd_backend = systemd 設定済み) |
| piユーザーのsudoはNOPASSWD | full-upgradeで 010_pi-nopasswd が消えパスワード必須化(今回の環境での実測) |
| [::]:22 退治にはssh.socketのdrop-inが必要(Ubuntu系) | 今回の実機はサービス直起動で、AddressFamily inet だけで済んだ |
まとめ
セットアップ直後のRaspberry Pi 5は、検証用の簡易なパスワードでパスワード認証が有効・ファイアウォールなし・更新可能パッケージ172件・意図しないIPv6アドレス保持、という状態でした。
今回の作業で、SSHは鍵のみ・接続元LAN限定になり、UFWは受信を原則拒否してSSHだけを自宅LANから明示的に許可する構成になり、unattended-upgradesの対象となる更新は自動適用されるようになりました。
作業はすべてSSH越しで、モニターは一度も繋いでいません。
いちばんの収穫は、sysctlのIPv6無効化がNetworkManagerの接続プロファイルに書き戻される現象を、実再起動の検収で捕まえられたことです。設定直後の確認だけで完了にしていたら、この巻き戻りには公開後まで気づけないところでした。
次回はいよいよ、このラズパイの上のWebサーバーを、受信ポートを一切開けないままWireCanalでインターネット側から使えるようにします。
それでは、また次回をお楽しみに!
出典・参考
- Raspberry Pi Documentation | Configuration(公式ドキュメント)
- fail2ban 公式リポジトリ(GitHub)
- UFW - Community Help Wiki(Ubuntu公式Wiki)
- NetworkManager 公式ドキュメント(freedesktop.org)