はじめに
「サーバーを立てれば、ファイアウォールくらい最初から入っているもの」――そう思っていませんか?
連載第1回で使った検証用 VPS を改めて調べたところ、Rocky Linux 9 の標準ファイアウォールである firewalld が、そもそもインストールされていませんでした。VPS のテンプレート側で削除されていたのです。その間、SSH(22番)と rpcbind(111番)が、ホスト側のファイアウォールなしで待ち受けている状態でした。
この記事では、firewalld のインストールから始めて、最終的に「SSH のみ許可」の最小構成にするまでの手順を、実機で確認しながら進めます。途中で「デフォルトでは Cockpit が許可されている」という注意点や、設定が保存される仕組み(runtime と permanent の違い)、そして最後には、わざと新規 SSH 接続を締め出して復旧する実験まで含めました。
対象読者:Rocky Linux 9 系を運用している方、VPS や小規模サーバーのファイアウォールを見直したい情シス・サーバー管理者の方。
結論(先に要点)
- VPS のテンプレートによっては firewalld 自体が入っていないことがある。設定の前に、まず有無の確認から始める
- インストール直後のデフォルトの許可サービスは
cockpit/dhcpv6-client/sshの3つ。「入れただけ」では最小構成にはならない - 設定変更は
--permanentを付けて保存し、--reloadで反映させるのが基本形。runtime だけの変更は reload で消える(実演で確認) - 不要なサービスを外した最終状態は services: ssh のみ。到達性は外側の PC から
Test-NetConnectionで確認し、22番=True(到達可)/111番=False(到達不可)の対が取れた - rpcbind 自体は止めず、firewalld で「外から 111 番に届かなくする」のがこの記事の範囲。rpcbind の停止手順は今回検証していないので扱わない
検証環境
- VPS:WebARENA Indigo
- OS:Rocky Linux release 9.8 (Blue Onyx)(第1回の検証後にフルアップデートで 9.8 になりました)
[root@server ~]# cat /etc/redhat-release
Rocky Linux release 9.8 (Blue Onyx)
まず現状確認:firewalld が入っていなかった
最初に、firewalld の有無を確認します。
[root@server ~]# dnf list installed firewalld
Error: No matching Packages to list
[root@server ~]# systemctl status firewalld
Unit firewalld.service could not be found.
インストールされていません。なぜ無いのかは dnf history に証拠が残っていました。
[root@server ~]# dnf history
ID | Command line | Date and time | Action(s) | Altered
---------------------------------------------------------------------------------------------------
4 | -C -y remove firewalld --setopt=clean_requir | 2023-11-13 16:06 | Removed | 10 EE
2023年11月、VPS イメージの作成時点で firewalld が削除されています。つまりこのサーバーは、ホスト側のファイアウォールが無い状態で提供されていました。「OS の標準ファイアウォールだから入っているはず」という思い込みは危険、というのが最初の教訓です。
続いて、今どのポートが待ち受けているかを確認します。
[root@server ~]# ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=4790,fd=7))
LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=642,fd=4),("systemd",pid=1,fd=59))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=4790,fd=8))
LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=642,fd=6),("systemd",pid=1,fd=61))
待ち受けは2つだけです。
- 22番(sshd)……SSH。用があるので当然
- 111番(rpcbind)……次節で説明する portmapper の窓口。使っている自覚がないのに開いている、というのが今回のポイント
Cockpit(Web 管理画面)の有無も確認しておきます。
[root@server ~]# systemctl status cockpit.socket
○ cockpit.socket - Cockpit Web Service Socket
Loaded: loaded (/usr/lib/systemd/system/cockpit.socket; disabled; preset: disabled)
Active: inactive (dead)
Triggers: ● cockpit.service
Listen: [::]:9090 (Stream)
Cockpit 本体はインストールされていますが、ソケットは無効(disabled)で停止中。ss の結果にも 9090 は出ていないので、今のところ 9090 は開いていません(Cockpit 自体の停止・削除は次回以降の記事で扱います)。
用語整理:portmapper と rpcbind と 111番ポート
111番の正体を理解するために、用語を整理します。無効化や遮断を勧める前に「それが何者か」を押さえるのが大事だと思っています。
portmapper は「RPC サービスの受付窓口」です。
RPC(Remote Procedure Call)を使うサービスは、SSH=22 のような固定ポートを持たず、代わりに「プログラム番号」で呼ばれます。たとえば NFS は 100003、portmapper 自身は 100000、という具合です。
- RPC サービスが起動すると、portmapper に「私(番号 100003)はポート 2049 にいます」と登録する
- クライアントは、まず 111 番の portmapper に「100003 はどこ?」と聞いてから、本体のポートへ接続する
会社の代表電話にたとえると分かりやすいでしょう。外線番号(111番)は固定で公開されていて、内線(実際のポート)は代表が案内する。その案内係が portmapper です。
名前が3つ出てくるので対応を押さえておきます。
- portmapper ……役割・仕組みの名前
- rpcbind ……今の Linux で portmapper の役割を実装しているプログラムの名前
- 111 ……portmapper 用に固定された窓口ポート
Rocky Linux では、NFS などがいつでも使えるように rpcbind が初期状態で動いています(正確には、接続が来たら起動できるようソケット側で待ち受けています)。ただ、この窓口が意味を持つのは、登録する RPC サービスがいてこそです。
実際に登録状況を見てみます。読み取り専用の確認コマンドです。
[root@server ~]# rpcinfo -p localhost
program vers proto port service
100000 4 tcp 111 portmapper
100000 3 tcp 111 portmapper
100000 2 tcp 111 portmapper
100000 4 udp 111 portmapper
100000 3 udp 111 portmapper
100000 2 udp 111 portmapper
登録されているのは portmapper(100000)自身だけでした。つまりこのサーバーでは、rpcbind に依存しているサービスは存在しません。案内係しかいない受付、という状態です。それでも開けっ放しにしておくと、外部から「このサーバーでは何の RPC が動いているか」を探る足がかりにされうる、というのが「使っていないなら塞ぐ/止める対象」と言われる理由です。
なお、rpcbind を止めて困るのは RPC を使うもの(NFS、NIS、一部のレガシー/バックアップ系)だけです。Web や SSH、メールは固定ポートで動く別物なので影響しません。
ただしこの記事では、rpcbind 自体は止めません。停止の手順は今回検証していないためです。本記事の範囲は「firewalld を入れて、外から 111 番に届かなくする」ところまで、と線を引きます。
firewalld のインストールと有効化
ではインストールします。本体に加えて、依存関係で nftables など関連パッケージも入ります。
[root@server ~]# dnf install -y firewalld
(途中省略)
================================================================================
Package Architecture Version Repository Size
================================================================================
Installing:
firewalld noarch 1.3.4-20.el9_8 baseos 452 k
Installing dependencies:
firewalld-filesystem noarch 1.3.4-20.el9_8 baseos 8.9 k
ipset x86_64 7.11-11.el9_5 baseos 41 k
ipset-libs x86_64 7.11-11.el9_5 baseos 68 k
iptables-nft x86_64 1.8.10-11.el9_5 baseos 187 k
libnftnl x86_64 1.2.6-4.el9_4 baseos 87 k
nftables x86_64 1:1.0.9-9.el9_8 baseos 453 k
python3-firewall noarch 1.3.4-20.el9_8 baseos 355 k
python3-nftables x86_64 1:1.0.9-9.el9_8 baseos 43 k
Installing weak dependencies:
libcap-ng-python3 x86_64 0.8.2-7.el9 appstream 29 k
Transaction Summary
================================================================================
Install 10 Packages
Total download size: 1.7 M
Installed size: 6.4 M
(途中省略)
Complete!
計10パッケージが入りました。続いて有効化して、状態を確認します。
[root@server ~]# systemctl enable --now firewalld
[root@server ~]# firewall-cmd --state
running
ファイアウォールを有効化する操作で一番怖いのは「その瞬間に今の SSH セッションが切れること」ですが、この検証では既存セッションは切れず、そのままコマンド実行を続行できました。さらに、別のターミナルから新規の SSH ログインができることも確認済みです(デフォルトのゾーンが SSH を許可しているためです。次節でその中身を見ます)。
デフォルト設定を確認する(cockpit が許可されている)
有効化できたから終わり、ではありません。デフォルトで何が許可されているかを必ず確認します。
[root@server ~]# firewall-cmd --get-default-zone
public
[root@server ~]# firewall-cmd --list-all
public (active)
target: default
icmp-block-inversion: no
interfaces: eth0
sources:
services: cockpit dhcpv6-client ssh
ports:
protocols:
forward: yes
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
デフォルトゾーンは public、許可サービスは次の3つでした。
- ssh(22番)……必要
- cockpit(9090番)……要注意
- dhcpv6-client(DHCPv6 の応答を受けるためのもの)……環境による
問題は cockpit です。前節で確認したとおり、cockpit.socket は無効で 9090 は LISTEN していません。ところが firewalld 側では cockpit が「許可済み」になっています。つまり、将来 Cockpit を起動したら、その瞬間に 9090 が外からそのまま通る状態です。「今は開いていない」ことと「ファイアウォールで許可されている」ことは別、というのがここでのポイントです。
方針は単純で、「使っていないサービスの許可は外す」。次節の仕組みを使って cockpit を外し、最後に dhcpv6-client の要否も判断して、ssh だけに絞ります。
runtime と permanent:reload で設定が「戻る」理由
firewalld の設定には2種類あります。
- runtime ……今この瞬間に効いている設定。変更は即座に反映されるが、保存されていない
- permanent ……ファイルに保存された設定。サービスの起動時や reload 時に読み込まれる
firewall-cmd の変更は、既定では runtime に対して行われます。ここを理解していないと「設定したはずなのに、再起動したら元に戻っていた」という事故になります。cockpit の除去で、実際に挙動を実演してみました。
まず runtime だけで cockpit を外します。
[root@server ~]# firewall-cmd --remove-service=cockpit
success
[root@server ~]# firewall-cmd --list-services
dhcpv6-client ssh
cockpit が消えました。ところが、ここで reload すると――
[root@server ~]# firewall-cmd --reload
success
[root@server ~]# firewall-cmd --list-services
cockpit dhcpv6-client ssh
cockpit が復活しました。 reload は「permanent の内容で runtime を読み直す」動作なので、保存されていない runtime の変更は消えてしまうのです。
今度は --permanent を付けて保存してから reload します。
[root@server ~]# firewall-cmd --permanent --remove-service=cockpit
success
[root@server ~]# firewall-cmd --reload
success
[root@server ~]# firewall-cmd --list-services
dhcpv6-client ssh
今度は reload 後も cockpit は消えたままです。firewalld の変更の基本形は、この2ステップだと覚えておくと安全です。
--permanentを付けて変更する(保存)--reloadで反映する
逆に、runtime だけの変更は「お試し用」と割り切ると、この後のコラムのような実験にも使えます。
SSH のみに絞る(dhcpv6-client の要否判断)
残るは dhcpv6-client です。これは DHCPv6 で IPv6 アドレス等の設定を受け取るクライアント側の通信(UDP 546番)を許可するサービスです。要るかどうかは、そのサーバーが IPv6 アドレスを DHCPv6 で受け取る構成かどうかで決まります。
実機の IPv6 の付き方を確認します。
[root@server ~]# ip -6 addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fe80::213:5dff:fe13:aad/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
eth0 に付いているのは link-local アドレス(fe80::/64)だけで、グローバルな IPv6 アドレスはありません。つまりこの環境は DHCPv6 でアドレスを受け取る構成ではないため、dhcpv6-client は不要と判断しました。
注意:この判断はこの検証環境でのものです。読者の環境で IPv6 アドレスを DHCPv6 で取得している場合は、外すと IPv6 側の通信ができなくなるおそれがあります。外す前に必ず
ip -6 addrで自分の環境を確認してください。
では外します。cockpit と同じく、permanent への変更+reload です。
[root@server ~]# firewall-cmd --permanent --remove-service=dhcpv6-client
success
[root@server ~]# firewall-cmd --reload
success
[root@server ~]# firewall-cmd --list-all
public (active)
target: default
icmp-block-inversion: no
interfaces: eth0
sources:
services: ssh
ports:
protocols:
forward: yes
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
services: ssh のみになりました。これがこの記事の到達点です。
外側から確認する(22番は通り、111番は届かない)
サーバー上の設定だけ見て満足せず、外側の PC から実際に届くかどうかを確認します。手元の Windows PC の PowerShell で Test-NetConnection を使います(以下、サーバーの IP アドレスは伏せて表記します)。
まず SSH の 22 番。
PS C:\> Test-NetConnection 203.0.113.10 -Port 22
ComputerName : 203.0.113.10
RemoteAddress : 203.0.113.10
RemotePort : 22
InterfaceAlias : Wi-Fi
SourceAddress : 192.168.40.69
TcpTestSucceeded : True
TcpTestSucceeded : True、到達できます。次に 111 番。
PS C:\> Test-NetConnection 203.0.113.10 -Port 111
警告: TCP connect to (203.0.113.10 : 111) failed
警告: Ping to 203.0.113.10 failed with status: TimedOut
ComputerName : 203.0.113.10
RemoteAddress : 203.0.113.10
RemotePort : 111
InterfaceAlias : Wi-Fi
SourceAddress : 192.168.40.69
PingSucceeded : False
PingReplyDetails (RTT) : 0 ms
TcpTestSucceeded : False
TcpTestSucceeded : False、到達できませんでした。
ここで注目したいのは、False が返るまでの挙動です。このテストは結果が出るまでかなり待たされました。firewalld のデフォルトの動作は、許可されていない通信を**応答を返さずに捨てる(DROP)**ためです。一般に、ファイアウォールが無く単にポートが閉じているだけなら、OS が即座に接続拒否を返すのでテストはすぐ終わります。黙って捨てられると、相手は応答を待ち続けて再送を繰り返し、やがてタイムアウトして失敗になります。待たされてから失敗する、その待ち時間自体が DROP のサインです。
そして大事な対比として、111 番はサーバー上では今も LISTEN したままです(最初の ss の結果どおり、rpcbind は止めていません)。「プロセスが待ち受けている」ことと「外から届く」ことは別であり、その間を塞ぐのがファイアウォールの仕事だ、という確認が取れました。
コラム:締め出し実験 ― runtime の ssh を消して、reload で復旧する
firewalld の設定ミスで一番怖いのは、SSH を締め出して自分が入れなくなることです。この検証環境で、あえてそれを再現し、「接続済みセッションから復旧できるか」を実測しました。安全設計つきでやるのがポイントです。
手順0:復旧の生命線と、安全装置を用意する
まず、permanent 側に ssh が残っていることを確認します。復旧は「permanent の内容を reload で読み直す」ことで行うので、これが生命線です。
[root@server ~]# firewall-cmd --permanent --list-services
ssh
次に安全装置として、10分後に自動で firewall-cmd --reload が実行されるタイマーを仕掛けます。万一このセッションごと落ちても、最大10分で ssh ありの状態に戻ります。
[root@server ~]# systemd-run --on-active=10min --unit=fw-safety /usr/bin/firewall-cmd --reload
Running timer as unit: fw-safety.timer
Will run service as unit: fw-safety.service
[root@server ~]# systemctl list-timers fw-safety.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2026-11-05 14:53:26 UTC 9min left - - fw-safety.timer fw-safety.service
手順1:壊す(runtime のみ)
作業中のセッションは絶対に閉じず、そのまま runtime の ssh を外します。--permanent は付けません。
[root@server ~]# firewall-cmd --remove-service=ssh
success
[root@server ~]# firewall-cmd --list-services
許可サービスが空になりました。この状態で確認できたことは2つです。
- 接続済みのセッションは生きている:このとおり、その後もコマンドを実行し続けられました
- 新規接続は締め出される:自宅 PC から
Test-NetConnection(IP) -Port 22を実行すると、今度はTcpTestSucceeded : False(待たされてから接続失敗)になりました
手順2:生きているセッションから復旧する
復旧は reload 一発です。
[root@server ~]# firewall-cmd --reload
success
[root@server ~]# firewall-cmd --list-services
ssh
permanent に残っていた ssh が runtime に戻りました。自宅 PC から再度 Test-NetConnection を実行すると TcpTestSucceeded : True に復帰。安全装置のタイマーが発動する前に、手動での復旧を完遂できました。最後に後片付けとして systemctl stop fw-safety.timer でタイマーを止め、firewall-cmd --list-all で最終状態(services: ssh)が維持されていることを再確認して終了です。
この実験から得た教訓は4つです。
- 作業セッションは閉じない。接続済みセッションが残っていれば、そこが復旧の足場になる
- 壊す前に、自動で元に戻る仕掛けを入れる(今回は systemd のタイマーで10分後に reload)
- 壊すのは runtime だけ。
--permanentを付けて ssh を消したり、壊れた runtime を--runtime-to-permanentで保存したりすると、復旧手段そのものが消える - 復旧は reload。permanent に正しい設定が残っていることが前提
まとめ
- 検証した VPS では firewalld がテンプレート段階で削除されていた。「標準だから入っているはず」と思い込まず、まず有無を確認する
- デフォルトの許可は cockpit / dhcpv6-client / ssh の3つ。cockpit は、起動していない今は開いていなくても「許可はされている」状態だった
- 設定変更は
--permanentで保存し--reloadで反映する。runtime のみの変更は reload で消える(復活する)ことを実演で確認した - dhcpv6-client は
ip -6 addrの結果(link-local のみ)から不要と判断。最終状態は services: ssh のみ - 外側からの確認では 22番=True/111番=False の対が取れた。111番のプロセス自体は止めておらず、firewalld で届かなくした
- 締め出しは「安全装置+runtime だけを壊す+作業セッションを残す」で安全に実験でき、reload で復旧できることを実証した
次回は、Cockpit 本体の停止・アンインストールを扱う予定です。
