誰も気づかないまま開放されたサーバー
先週、クライアントのUbuntuサーバーをレビューした。PHPのWebアプリとMySQLが動いているサーバーだ。外部インターネットからnmap -sV <ip>を実行してみると、3306(MySQL)、6379(Redis)、8080(開発サーバー)など、あらゆるポートが外部に開放されていた。ファイアウォールは一切なし。
よくある話だ。サーバーは問題なく動いていて、警告も出ないから、ファイアウォールの設定はずっとToDoリストの下に追いやられる。MySQLがブルートフォース攻撃を受けるか、Redisがcryptominerに使われて初めて、慌てることになる。
意図しないのに「開放」されてしまう理由
MySQLやRedisなど、どのサービスをインストールしても、デフォルトでは0.0.0.0にバインドされる。つまり、インターネット接続インターフェースを含む、すべてのネットワークインターフェースで待ち受けるということだ。ファイアウォールで遮断しなければ、誰でも接続を試みることができる。
Linuxカーネルにはnetfilterが組み込まれている。カーネルレベルでパケットをフィルタリングするフレームワークだ。iptablesはnetfilterを設定するためのコマンドラインツールである。UbuntuやCentOSを新規インストールした直後は、iptablesはデフォルトですべてACCEPT状態になっており、何もブロックしない。
もう一つの問題:多くの管理者がufw(Ubuntu)やfirewalld(CentOS/RHEL)をブラックボックスとして使っている。コマンドは知っていても、内部の仕組みは理解していない。NAT、ポートフォワーディング、高度なレート制限が必要になると、これらのラッパーツールは限界を見せ始める。iptablesを直接理解しておくと、そういった状況でも適切な場所でデバッグできる。
作業を始める前にiptablesの構造を理解する
iptablesはルールをテーブルとチェーンで管理する。最もよく使われるテーブルはfilterで、3つのチェーンを持つ:
- INPUT:このサーバーへの受信パケット
- OUTPUT:このサーバーからの送信パケット
- FORWARD:サーバーを通過するパケット(サーバーをルーター/ゲートウェイとして使う場合)
各ルールにはパケットの行き先を決めるターゲットがある:
ACCEPT:通過を許可するDROP:通知なしに静かにブロックするREJECT:ブロックしてエラー通知を返すLOG:ログに記録して次のルールの処理を続ける
ルールは上から順に評価される。最初にマッチしたルールが適用され、それ以降のルールは処理されない。
ufwとiptables直接設定、どちらを使う?
方法1:ufwを使う — シンプルだが制限あり
固定ポートをいくつか開けるだけの小規模サーバーには十分:
sudo ufw enable
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose
メリット:速くて覚えやすい。デメリット:NAT、レート制限、高度なルールの処理が難しい。特殊なケースに直面すると、結局iptablesに戻ることになる。
方法2:iptablesを直接設定する — 完全なコントロール
これが私が本番サーバーで使う方法だ。少し複雑だが、細部まで完全にコントロールできる。
本番Webサーバー向けiptables完全スクリプト
バラバラにコマンドを叩いて何をしたか忘れるより、スクリプトを書いておけば管理しやすく、必要なときに再デプロイもできる。私が管理しているUbuntu 22.04サーバーでは、1時間に数百件のボット接続がブロックされている。ファイアウォールがなければ、それらすべてがアプリケーションに届いてしまう。
#!/bin/bash
# firewall.sh — Webサーバー向けiptables設定
# 既存のルールをすべて削除
iptables -F
iptables -X
iptables -Z
# デフォルトポリシー:INPUTとFORWARDをすべてDROP
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# ループバックを許可(localhost — 必須)
iptables -A INPUT -i lo -j ACCEPT
# ステートフルファイアウォール:established接続を許可
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# SSHを許可(DROPポリシー設定前に必ず追加すること)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# HTTPとHTTPSを許可
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# pingを許可
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
# DROPされたパケットをデバッグ用にログ記録
iptables -A INPUT -j LOG --log-prefix "IPT-DROP: " --log-level 4
iptables -A INPUT -j DROP
echo "Firewall configured!"
iptables -L -v --line-numbers
重要な警告:SSHでサーバーに接続中の場合、--dport 22のルールはデフォルトポリシーをDROPに設定する前に追加しなければならない。順序を間違えると、即座に自分自身をロックアウトしてしまう。
初回テスト時の安全対策:万が一SSHをロックアウトしてしまった場合に備え、5分後に自動リセットするコマンドを設定しておく:
echo "iptables -F && iptables -P INPUT ACCEPT" | at now + 5 minutes
(atがインストールされていない場合は先にインストール:sudo apt install at)
レート制限によるSSHブルートフォース対策
すべてのSSH接続をACCEPTするのではなく、パスワードスキャンボットをブロックするレート制限を追加する:
# 上記のシンプルなSSHルールをこちらに置き換える:
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
-m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
このルールは、60秒以内に4回以上の新規SSH接続を試みるIPアドレスをブロックする。
MySQLとRedisをロック — localhostまたは内部IPのみ許可
# MySQL:localhostからのみ許可
iptables -A INPUT -p tcp --dport 3306 -s 127.0.0.1 -j ACCEPT
# または内部IPレンジから(VPC、プライベートネットワーク)
iptables -A INPUT -p tcp --dport 3306 -s 10.0.0.0/8 -j ACCEPT
# Redis:同様に
iptables -A INPUT -p tcp --dport 6379 -s 127.0.0.1 -j ACCEPT
# この2つのポートへの残りの通信をすべてブロック
iptables -A INPUT -p tcp --dport 3306 -j DROP
iptables -A INPUT -p tcp --dport 6379 -j DROP
再起動時に自動ロードされるようにルールを保存する
iptablesのルールは保存しないと再起動後に消えてしまう。これは多くの人が忘れがちな点だ:
# Ubuntu/Debian
sudo apt install iptables-persistent
sudo netfilter-persistent save
# 保存されたファイルを確認
cat /etc/iptables/rules.v4
# CentOS/RHEL 7
sudo service iptables save
# ルールは /etc/sysconfig/iptables に保存される
よく使う確認・デバッグコマンド
# すべてのルールを番号とパケット/バイト数付きで表示
iptables -L -v --line-numbers
# 特定のテーブルのルールを表示
iptables -t nat -L -v
# 番号でルールを削除(例:INPUTの3番目のルールを削除)
iptables -D INPUT 3
# DROPされたログを確認
dmesg | grep "IPT-DROP"
# または
tail -f /var/log/kern.log | grep "IPT-DROP"
# すべてをデフォルトにリセット(やり直したいとき)
iptables -F && iptables -P INPUT ACCEPT && iptables -P FORWARD ACCEPT
iptables設定でよくある落とし穴
- SSHのロックアウト:必ずtmux/screen内でテストし、上記の自動リセットコマンドと組み合わせること。
- 再起動後にルールが消える:
iptables-persistentをインストールしてnetfilter-persistent saveを実行することを忘れずに。 - ufw/firewalldとの競合:ufwを使用中の場合、iptablesを直接使う前に無効化すること:
sudo ufw disable。 - DockerによるFirewallの迂回:Dockerは自動的にiptablesにルールを追加し、FORWARDポリシーを無効化する可能性がある。Dockerを使う場合は
DOCKER-USERチェーンの追加設定が必要。 - ESTABLISHEDルールの欠如:
--ctstate ESTABLISHED,RELATEDの行を忘れると、既存の接続がDROPされ、Webサイトはヘッダーをロードしたあとにハングアップする。
まとめ
iptablesは設定したら終わりではない。新しいサービスを追加するたびに自問する必要がある:このポートを外部に公開する必要があるか?デフォルトはNoだ。上記のテンプレートスクリプトはほとんどのWebサーバーに対応できる。アプリケーションに応じてルールを追加・削除してほしい。
新しいサーバーを引き継いだときに私がいつも実践する原則:まず外部からnmap -sV <ip>を実行する。存在する理由のないポートが見つかったら、即座に閉じる。MySQL、Redis、Elasticsearch、Memcachedはすべてlocalhostまたは内部ネットワークからのみ待ち受けるべきだ。
