aegis-firewall-edge について
aegis-firewall-edgeは、稼働中のLinuxホストまたはその構成コードのネットワーク境界を監査するスキルです。リスナーや有効なファイアウォール、送信通信、IPv6、管理面を確認し、Dockerやクラウドのセキュリティグループとの相互作用も調べます。
主な機能
- 01TCP・UDPリスナーの一覧化
- 02有効なファイアウォールルールの確認
- 03デフォルト拒否と送信制御の評価
- 04IPv6ルールの整合性確認
- 05レート制限と管理面の分離確認
- 06DockerとクラウドSGの相互作用調査
活用シーン
- 01Linuxホストの管理ポート露出の確認
- 02IPv6やDockerによるポート迂回の発見
- 03送信制御とブルートフォース対策の評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ as-foss/ aegis-skills --skill aegis-firewall-edgeインストール後、Claude Code で/aegis-firewall-edgeを入力して読み込まれたか確認します。
同梱ファイルreferences/background.mdreferences/further-reading.md
詳細
対象範囲と目的
稼働中の Linux ホスト 1 台のネットワークエッジ(またはその構成をコード化したもの)を監査します。優先順位の高い順に、次の 8 つの領域を対象とします。
- 公開範囲のインベントリ — すべての TCP/UDP リスナーについて、バインドアドレス、サービス、ユーザーを特定し、インターネットから到達可能、ループバックのみ、リンクローカルに分類します。ループバックにすべきワイルドカード(
0.0.0.0/[::])バインドも特定します。 - ファイアウォールの状態 — nftables/iptables/ufw/firewalld のうち、実際に有効なもの(または有効なものがない状態)、デフォルトポリシー、チェーンごとのルール密度を確認します。
- デフォルト拒否の設計 — インバウンドポリシーでドロップし、明示的に許可する構成、レート制限付きのドロップログ、3 つすべての管理レイヤーに対応する正しい構文を確認します。
- エグレスフィルタリング — SSRF による外部送信、リバースシェル、データ窃取を防ぐアウトバウンド制御と、段階的なログのみでの導入を確認します。
- IPv6 の同等性 — IPv6 を IPv4 と同等以上に厳しくフィルタリングしつつ、NDP/RA を許可します。
- L4 レート制限とブルートフォース対策 — ファイアウォールで接続数/レートを計測し、sshd と nginx の認証失敗に対して fail2ban の jail を設定します。
- 管理プレーンの分離 — SSH、DB 管理、監視エージェントへの接続を VPN/踏み台の CIDR からのみに制限し、ロックアウトを防ぐ変更手順を設けます。
- レイヤー間の相互作用 — Docker の DNAT による INPUT ルールの迂回、クラウドのセキュリティグループとホストファイアウォールによる二重の公開、または誤った安心感を確認します。
対象外(関連項目):カーネルの sysctl 設定(SYN cookies、tcp_syncookies、conntrack の上限)→ BASE/linux-baseline、TLS 終端とプロキシヘッダー → TLS、コンテナーランタイムのサンドボックス化 → SANDBOX、ログ転送 → LOGMON。
運用ルール:
- すべての調査コマンドは読み取り専用です。変更を伴うコマンドは、承認後の Remediation セクションにのみ記載します。
- 設定ファイルよりも実効状態の証拠(
ss -tlnp、nft list ruleset)を優先します。ディスク上のルールセットファイルだけでは、何が読み込まれているかは分かりません。 - root 権限が必要なコマンドには
[ROOT]を付けます。root 権限がない場合は、一般ユーザーが読み取り可能な状態と構成リポジトリを監査し、Patterns & Signatures を使ってレンダリング済み設定を評価します。 - ホスト外部からの接続による外部プロービングには、接続元ホストと対象範囲を明記した書面による許可が必要です。許可がない場合は、Exploitation & Reproduction に従い、ホスト内の証拠を使用します。
前提条件と用語
予備知識がなくても理解できるよう、このモジュールで使用する用語を各 1 行で説明します。各クラスの詳しい平易な説明は、リポジトリの GUIDE.md の用語集にあります。
- デフォルト拒否:明示的に許可されたものを除き、すべてのトラフィックをドロップすること
- リスナーバインド:サービスが接続を受け付けるアドレス。
0.0.0.0はすべてのインターフェースを意味します - セキュリティグループ(SG):ホストの前段にあるクラウドレベルのファイアウォール
- Docker DNAT の迂回:公開されたコンテナーポートが通常のホストファイアウォールルールを迂回して転送されること
- エグレスフィルタリング:盗まれたアクセス権による外部への通信や情報流出を防ぐため、アウトバウンド先を制限すること
- IPv6 の同等性:IPv6 を IPv4 と同等以上に厳しくフィルタリングすること
- fail2ban jail:ログイン失敗が繰り返された後に IP を自動的にブロックする仕組み
- 汚染フロー/ソース→シンク:信頼できないデータが、入口から危険な関数まで伝わる経路
- 検出事項のステータス:確認済み > 可能性が高い > 要レビュー。証拠のルールは templates/finding-report.md にあります
メンタルモデル
露出は独立した4つのレイヤーで決まります。どれか1つでも穴があれば十分です。
internet ──▶ [1] cloud SG/NLB filters by CIDR/port BEFORE the host
──▶ [2] host firewall nftables/iptables INPUT (+ FORWARD for docker)
──▶ [3] listener binding which address the process accepts on
──▶ [4] app auth passwords, ACLs, TLS client certs
このモジュールで利用する主な非対称性:
- バインドは、「すべてのフィルターがなくなった場合に誰が到達できるか」を示す実際の基準です。 ワイルドカードバインド(
0.0.0.0:6379)は、レイヤー3がサービスをインターネット全体に提供することを意味します。これを防ぐのはレイヤー1~2だけです。ループバックバインド(127.0.0.1:6379)には、ファイアウォールの設定ミスにかかわらず到達できません。だからこそ、ワイルドカードバインドはファイアウォールルールの欠如ではなく、クラウド侵害の最大の経路でした(Redis/Memcached/MongoDBを狙ったランサムウェア攻撃の波)。 - Dockerはレイヤー2をすり抜けます。 公開ポート(
-p 6379:6379)はnatテーブルでDNATされ、FORWARDチェーン経由で転送されます。ufw/nftablesのINPUTルールでは、このトラフィックを確認できません。 - ホストファイアウォールは4つのうちレイヤー2であり、SGの代わりにはなりません。 SGが開いていてホストも開いていると、二重に露出します。SGが閉じていてホストが開いている状態は、トポロジーが変わると(新しいサブネット、ピア接続されたVPC、LBの接続など)消えてしまう誤った安心感です。
- インバウンドには注目が集まりますが、エグレスは見落とされがちな半分です。 足掛かり(SSRF、Webシェルなど)を得た後、フィルタリングされていない送信経路によって、アクセスがデータ流出、リバースシェル、ラテラルムーブメントへとつながります。
- IPv6は2つ目の玄関です。
ipファミリーだけを対象に構築されたルールセットでは、table inetがないホストの[::]リスナーが完全に無防備になります。攻撃者はIPv4と同じようにIPv6もスキャンします。
各リスナーを最悪の場合の到達可能性で分類し、レイヤー1~2が実際に制限しているか確認してください。両方のレイヤーを照合せずに「SGがブロックしている」と決めつけてはいけません。
確認事項
1. 露出状況の一覧作成(最初に実施)
[ROOT]で、以下のすぐに貼り付けて実行できるコマンド一式を実行し、ss -tlnpとss -ulnpの各行について、バインドアドレス:ポート → プロセス名 → 所有UID → 分類をまとめた表を作成します。
# ===== exposure sweep (read-only; run as root) =====
echo "== TCP listeners =="; ss -tlnp
echo "== UDP listeners =="; ss -ulnp
echo "== wildcard-bound TCP listeners (suspect) =="
ss -tlnp | awk 'NR==1 || $4 ~ /^0\.0\.0\.0:/ || $4 ~ /^\[::\]:/ || $4 ~ /^\*:/'
echo "== firewall backend detection =="
for s in nftables ufw firewalld iptables netfilter-persistent; do
printf '%-24s %s\n' "$s:" "$(systemctl is-active "$s" 2>/dev/null)"
done
command -v nft >/dev/null && nft list tables
command -v iptables >/dev/null && iptables -V
command -v ufw >/dev/null && ufw status verbose
command -v firewall-cmd >/dev/null && firewall-cmd --state
echo "== iptables chain policies + INPUT rule count =="
iptables -S 2>/dev/null | grep '^-P'
printf 'INPUT rules: %s\n' "$(iptables -S INPUT 2>/dev/null | grep -c '^-A')"
echo "== ipv6 parity quick check =="
ip6tables -S 2>/dev/null | grep '^-P'
printf 'ip6tables INPUT rules: %s\n' "$(ip6tables -S INPUT 2>/dev/null | grep -c '^-A')"
nft list tables 2>/dev/null | grep -qE '(^| )(inet|ip6) ' || echo "no inet/ip6 nft table -> IPv6 likely unfiltered"
echo "== docker presence =="
[ -S /var/run/docker.sock ] && echo "docker socket present"
iptables -t nat -S DOCKER 2>/dev/null | head -5
分類ルール:
127.0.0.1/[::1]にバインドされている場合 → ループバックのみ。設計上、安全です。169.254.*またはリンクローカルIPv6にバインドされている場合 → リンクローカルのみ。記録しますが、インターネットから到達可能とは扱いません。10.*、172.16-31.*、192.168.*、またはプライベートインターフェイスIPにバインドされている場合 → 内部から到達可能です。これらの範囲を誰が所有しているか(VPN? VPC全体? 共有オフィスLAN?)も監査します。0.0.0.0、[::]、*、またはパブリックインターフェイスIPにバインドされている場合 → 上位のレイヤーによってブロックされない限り、インターネットから到達可能です。このようなリスナーには、それぞれ明確な正当性が必要です。
ワイルドカードバインドごとに、ループバックを越えて到達可能にする必要が本当にあるか確認します。Webサーバー(80/443)、パブリックAPIポート、ロードバランサーのバックエンドは正当な場合がありますが、データベース、キャッシュ、キュー、管理パネル、開発サーバーはそうではありません。users:(("name",pid=N,...))フィールドでプロセスの識別情報を照合してください。0.0.0.0で待ち受ける「node」や「python」は、忘れ去られた開発サーバーかもしれません。
2. ファイアウォールの状態評価
読み込まれたルールを実際に管理しているバックエンドを特定します。ディストリビューションによって異なり、複数の管理ツールが気付かれないまま競合することがあります。
[ROOT]でnft list tablesを実行します。有効なリスナーがあるのに出力が空の場合、ファイアウォールがまったくありません(それ自体が検出事項です)。iptables -Vを実行します。バージョンの接尾辞が(nf_tables)ならiptables-nft変換バックエンド、(legacy)なら従来のxtablesです。一方のバックエンドで読み込まれたルールは、もう一方からは見えません。iptables -Sを信頼する前に検出してください。[ROOT]でufw status verboseを実行します。Status: inactiveは検出事項です。Default:の行も記録してください。[ROOT]でfirewall-cmd --stateとfirewall-cmd --list-allを実行します。ゾーン、インターフェイス、サービス、リッチルールを記録してください。- 従来方式であることの確認:
update-alternatives --display iptables 2>/dev/null | grep value(Debian/Ubuntu)、lsmod | grep -E '^ip_tables|^nf_tables'。
ポリシーとルールの密度を評価します。
- チェーンポリシー:
iptables -S | grep '^-P'。nftでは、nft list rulesetの各ベースチェーンの行にあるpolicyキーワードを確認します。 - チェーンごとのルール数:
iptables -S INPUT | grep -c '^-A'。インターネットから到達可能なホストで、ポリシーがACCEPTかつルール数が0なら、空のファイアウォールとして検出します。ufw/firewalldのパッケージがインストールされていても同様です(インストール済み ≠ 有効)。 - Dropのログ記録の有無:
nft list rulesetでdrop判定より前にあるlog prefixルール、またはiptables -S INPUTの-j LOGを確認します。
3. デフォルト拒否設計
読み込まれた(ディスク上ではない)ルールセットが、ループバックの許可 → 確立済み/関連接続の許可 → 管理者 CIDR に制限した SSH → パブリックな 80/443 → それ以外すべてをレート制限付きでログ記録して破棄、という構成を実装していることを確認してください。FORWARD もデフォルトで破棄することを確認してください。Patterns & Signatures および Remediation の参照パターンと比較してください。意図した範囲より広い accept(例:送信元制限なしの tcp dport 22 accept)は、指摘事項です。
4. アウトバウンド通信の状態
output チェーンの方向を調査してください。iptables -S OUTPUT / nft output チェーンポリシーを確認します。制限のない policy ACCEPT は現在一般的です。これをベースラインの指摘事項(Medium アンカー)として記録し、足場を得た攻撃者が到達できる対象を評価してください。リゾルバー、パッケージミラー、アプリで既知の外部 API、そして特にクラウドのメタデータエンドポイントを確認します。メタデータへの到達性を調べるプローブ(無害な GET、許可された環境で実施):
curl -sS --max-time 3 http://169.254.169.254/latest/meta-data/ami-id && echo # AWS/GCP shape
curl -sS --max-time 3 -H 'Metadata:true' 'http://169.254.169.254/metadata/instance?api-version=2021-02-01' -o /dev/null -w '%{http_code}\n' # Azure shape
ワークロードのコンテキスト内から応答があれば、メタデータに到達可能であることが証明されます。これは SSRF からインスタンス認証情報へピボットする際の標的です。IMDSv2/トークンの強制が適用されているかを記録してください(AWS IMDSv2 のみを使用するインスタンスでは、盲目的な GET は 401 を返します)。
5. IPv6 の同等性
table inetが存在するか確認してください(nft list tables)。inetファミリーは v4 と v6 の両方を対象とします。table ip filterだけの場合は v4 のみを対象とします。- レガシー環境の場合:
iptables -S INPUT | grep -c '^-A'とip6tables -S INPUT | grep -c '^-A'を比較してください。 - リスナーテーブルを再スキャンし、
[::]:PORTの行を確認してください。v6 にファイアウォールがない場合、そのすべてがフィルタリングされていない v6 の入口です。 - NDP/RA の要件:v6 ファイアウォールでは、近隣要請/近隣通知およびルーター広告の ICMPv6 タイプを許可する必要があります。許可しないと v6 接続が通知なく機能しなくなります。プライバシー拡張(テンポラリーアドレス)はここでは関係ありません。サーバーでは静的アドレスを使用するため、それらを対象にしたフィルタリングは試みないでください。
6. レート制限と L4 保護
いずれかのレイヤーで、次の設定を確認してください。
- SSH のスロットリング:
ufw limit ssh形式のルール、nft メーター(meter ... { ip saddr ... })、ポート 22 の NEW 接続に対する iptables のrecent/hashlimitマッチ。 - パブリックサービスの SYN/新規接続レート上限(dport 80/443 に対する nft ルールの
limit rate)。 - ICMP のレート制限:echo-request を
limit rate節の対象にしつつ、destination-unreachable/time-exceeded は引き続き許可します。 - fail2ban:
systemctl is-active fail2banを実行し、続けて[ROOT]でfail2ban-client statusを実行します。どの jail が有効ですか?sshd jail はありますか?nginx ホストでは、認証失敗を監視する jail がありますか?
インターネットから到達可能なホストで、上記 4 項目すべてがなく、SSH がパスワード認証を使用している場合は、指摘事項です(BASE/linux-baseline の sshd チェックに加わります)。
7. 管理プレーンの分離
各管理用リスナー(sshd 22、DB 管理ポート、node_exporter 9100、phpMyAdmin、RabbitMQ 管理 15672、監視エージェント)について:
- インベントリテーブルから、バインド先アドレスを記録してください。
- 読み込まれたルールセットで、そのリスナーを許可するルールを見つけ、送信元セレクターを確認してください。管理者 CIDR/VPN 範囲に制限されているか、制限がないかを確認します。
- 観察されたオーバーレイ/ノッキングの選択肢(tailscale/wireguard インターフェース
ip link show type wireguard、knockd ユニット)を、十分な対策だと評価せずに、状況情報として記録してください。
管理ポートが 0.0.0.0/0 または、踏み台サブネットだけが必要なのに VPC CIDR 全体から許可されている場合は、指摘事項です。
8. Docker との相互作用
/var/run/docker.sock が存在する場合、または docker ps --format '{{.Names}}\t{{.Ports}}' がコンテナを返す場合:
- 公開ポートとホスト側のバインドを一覧にしてください。
-p 127.0.0.1:5432:5432と-p 5432:5432を比較します。 [ROOT]でiptables -t nat -S DOCKER | grep DNATを実行してください。ワイルドカードで公開されたポートはすべて、INPUT を完全にバイパスする DNAT としてここに表示されます。- DOCKER-USER チェーンが存在し、制限的なルールがあることを確認してください:
iptables -S DOCKER-USER。デフォルトでは末尾のRETURN以外は空です。つまり、ufw の INPUT ルールはコンテナ公開ポートを保護しません。 - compose ファイル/ユニット引数を相互確認してください。
"8080:8080"のようなポートマッピングと、127.0.0.1:が先頭に付いたものを比較します。
9. クラウドのセキュリティグループの調整
ホストから自身の SG を直接確認することはできません。以下の方法で調整します。
- コードとして管理されている設定リポジトリ(Terraform の
aws_security_group/google_compute_firewall/azurerm_network_security_groupブロック、またはアクセス権がある場合はクラウドコンソール)を読み、このインスタンスの NIC/LB ターゲットグループに適用される SG ルールを確認します。 - 統合マトリクスを作成します。ポートごとに、SG の状態 × ホストファイアウォールの状態 × バインドを確認します。調査結果:
- SG が開いており、ホストも開いていて、ワイルドカードバインドになっている → 二重の露出。認証されていないサービスの場合は重大です。
- SG が閉じていて、ホストが開いている → 安心は禁物です。トポロジーの変更(新しいピアリング、LB のアタッチ、一部の VPC での IPv6 のデフォルト開放)により、開いているホスト層が気付かないうちに有効になるため、指摘してください。
- SG が開いていて、ホストが閉じている → 通常は正しい設計です(エッジとしての SG)。ホスト層が「誤って」閉じていないか(誰も管理していない空のチェーン)確認してください。
確認箇所
証拠の収集:tools/sweeps/sweep-firewall.sh は [FW-nn] セクションをそのまま取得します。生の出力だけではなく、このモジュールの評価基準に照らして判断してください。
まず有効な状態を確認し、次にファイルを確認します(読み込まれた設定とファイルの内容が一致しない場合があります)。
| 成果物 | パス/コマンド | 備考 |
|---|---|---|
| 読み込み済みの nftables ルールセット | nft list ruleset [ROOT] | nft バックエンドの実際の状態 |
| nftables の永続設定 | /etc/nftables.conf;ドロップイン:/etc/nftables.d/*.nft | nftables.service によって読み込まれます |
| iptables-nft/legacy の実行中ルール | iptables -S、iptables -t nat -S、ip6tables -S [ROOT] | Docker の監査には nat テーブルが必須です |
| iptables の永続設定 | Debian:/etc/iptables/rules.v4、rules.v6;RHEL legacy:/etc/sysconfig/iptables、ip6tables | iptables-persistent/netfilter-persistent.service と iptables.service の違いに注意してください |
| ufw の状態 | ufw status verbose [ROOT] | ファイル:/etc/default/ufw(IPV6=yes?)、生成されたルール /etc/ufw/{before,user,after}.rules |
| firewalld | firewall-cmd --list-all --permanent [ROOT] | ゾーン:/etc/firewalld/zones/*.xml、ディストリビューションのデフォルト:/usr/lib/firewalld/zones/ |
| fail2ban | fail2ban-client status [ROOT];/etc/fail2ban/jail.local、jail.d/*.conf | ファイルのテキストではなく、クライアントを介して有効な jail を確認します |
| Docker の公開ポート | docker ps --format '{{.Names}}\t{{.Ports}}';iptables -t nat -S DOCKER [ROOT] | Compose ファイル:リポジトリ全体の ports: ブロック |
| リスナー/プロセスの対応表 | ss -tlnp、ss -ulnp [ROOT] | 同等のコマンドとして lsof -nP -iTCP -sTCP:LISTEN も使用できます |
| コードとして管理されているエッジルール | Terraform の *.tf セキュリティグループブロック;Ansible の ufw/iptables/firewalld タスク;cloud-init のユーザーデータ | 「パターンとシグネチャ」に従い、レンダリング後の出力を評価します |
パスを信頼する前に、cat /etc/os-release でディストリビューションを判別してください。Debian/Ubuntu では ufw+nftables、RHEL/Fedora では firewalld が主に使われます。Alpine では、管理ツールなしでプレーンな iptables/nftables を使用します。
パターンとシグネチャ
リスナー分類表
| リスナー | リスクのあるバインド | 想定されるバインド | 理由 |
|---|---|---|---|
| Redis 6379 | 0.0.0.0:6379、[::]:6379 | 127.0.0.1:6379 | デフォルトでは認証なし。過去に大規模な侵害が発生し、crontab の書き換えから RCE に至る一連の攻撃が確認されています |
| Memcached 11211 | 0.0.0.0:11211、UDP 有効 | 127.0.0.1:11211、-U 0 | データ窃取に加え、これまでに観測された最大規模の UDP 増幅型 DDoS の攻撃経路です |
| PostgreSQL 5432 | 0.0.0.0:5432、listen_addresses='*' | 127.0.0.1:5432 と unix ソケット | 認証情報のリスト型攻撃の標的になります。同一ホスト上でアプリを実行する場合、外部公開は不要です |
| MySQL/MariaDB 3306 | bind-address=0.0.0.0 | 127.0.0.1 またはプライベート CIDR | ブルートフォース攻撃の標的であり、過去には認証前に悪用可能な CVE もありました |
| MongoDB 27017 | 認証を有効にせず bindIp: 0.0.0.0 | 127.0.0.1/VPC 内部 + 認証 | ランサムウェア攻撃で公開状態の標的となった代表例です |
| RabbitMQ 管理 15672/AMQP 5672 | 0.0.0.0:15672 | ループバックまたは VPN 経由のみ | 管理パネルです。過去にはデフォルトの guest/guest がそのまま使われていました |
| Elasticsearch 9200/Kibana 5601 | network.host: 0.0.0.0 | ループバック/プライベート + 認証 | データがすべて窃取されるおそれがあります。旧バージョンにはスクリプト実行による RCE の履歴もあります |
| 開発/管理サーバー(Jupyter 8888、Werkzeug 5000、Vite/webpack 3000–8080) | 0.0.0.0 または --host=0.0.0.0 | 127.0.0.1 | デバッガーコンソールやファイルアップロード用エンドポイントがあり、セキュリティ強化がまったく施されていません |
| 監視エージェント(node_exporter 9100、dcgm など) | 0.0.0.0:9100 | プライベートインターフェイス、またはループバック+プロキシ | 内部メトリクスが漏えいし、スクレイピング用エンドポイントが列挙されるおそれがあります |
シグネチャ:ワイルドカードリスナーの公開
# VULNERABLE — ss -tlnp shape: wildcard binds on sensitive services
LISTEN 0 511 0.0.0.0:6379 0.0.0.0:* users:(("redis-server",pid=812,fd=7))
LISTEN 0 4096 [::]:27017 [::]:* users:(("mongod",pid=903,fd=21))
LISTEN 0 128 *:8888 *:* users:(("python",pid=1204,fd=4))
# FIXED — same services loopback-bound
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=812,fd=7))
LISTEN 0 4096 127.0.0.1:27017 127.0.0.1:* users:(("mongod",pid=903,fd=21))
LISTEN 0 128 127.0.0.1:8888 0.0.0.0:* users:(("python",pid=1204,fd=4))
シグネチャ:稼働中のホストでファイアウォールが空
# VULNERABLE — iptables -S on an internet-reachable host: policies ACCEPT, zero rules
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
# FIXED — default-deny shape with explicit allows present (excerpt)
-P INPUT DROP
-P FORWARD DROP
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -s 203.0.113.0/24 -p tcp --dport 22 -j ACCEPT
-A INPUT -j LOGDROP
シグネチャ:IPv6 の抜け穴
# VULNERABLE — v4 locked down but ip6tables empty/wide open; listeners on [::]
$ ip6tables -S INPUT
-P INPUT ACCEPT # zero '-A' lines follow
$ nft list tables
table ip filter # family 'ip' only -> v6 unfiltered
# FIXED
table inet filter { ... } # single inet table governs both families
ip6tables -P INPUT DROP # legacy path equivalent, mirrored rule set
受信トラフィックの設計例(3 種類の構文)
アプリケーションサーバーの設計意図:ループバックを許可し、確立済み/関連する接続を許可し、SSH は管理 CIDR 203.0.113.0/24 からのみ許可し、HTTP(S) は公開し、ICMP はレート制限し、それ以外はすべて(レート上限付きで)ログに記録して破棄します。標準的な完全版ファイルは Remediation にあります。ここではシグネチャを示します。
# FIXED — nftables (native)
tcp dport { 80, 443 } ct state new accept
ip saddr 203.0.113.0/24 tcp dport 22 ct state new accept
icmp type echo-request limit rate 5/second accept
meta l4proto ipv6-icmp accept
jump logdrop # chain: log under rate limit, then drop everything
# FIXED — ufw (same intent, command sequence)
ufw default deny incoming
ufw allow from 203.0.113.0/24 to any port 22 proto tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw logging on
ufw enable
# FIXED — firewalld rich rules (same intent)
firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="203.0.113.0/24" service name="ssh" accept'
firewall-cmd --permanent --zone=public --remove-service=ssh
firewall-cmd --permanent --zone=public --add-service=http --add-service=https
firewall-cmd --set-log-denied=all
firewall-cmd --reload
汚染追跡のガイダンス
「公開」を、設定ソースからインターネット到達可能なシンクへのデータフローとして扱います。
ソースパターン(危険な値の発生元):
- ワイルドカードバインドのリテラル:
0.0.0.0、[::]、*、--host 0.0.0.0、listen_addresses = '*'、bind 0.0.0.0、bindIpAll: true、network.host: 0.0.0.0。 - ワイルドカードに解決されるデフォルト値:未設定の
LISTEN_HOST、Compose のports: "8080:8080"、Docker の-p 8080:8080、Kubernetes のhostPort:/nodePort:。 - 広範なエッジルール:非 Web ポートに対する SG の
cidr_blocks = ["0.0.0.0/0"]または"::/0"、送信元セレクターのない nft/iptables の許可ルール。 - 送信トラフィックの制限がない状態:出力ポリシーが ACCEPT で、宛先を限定した許可ルールがありません。
設定をコードで管理するリポジトリで追跡する伝播経路:
- 変数のデフォルト値:エントリーポイント、systemd ユニット(
Environment=)、Helm の values.yaml にある${LISTEN_HOST:-0.0.0.0}など。変数が最終的な設定にどのように反映されるかを追跡します。 - テンプレートのレンダリング:バインドアドレスを埋め込む Ansible/Jinja テンプレートでは、テンプレート内の定数ではなく、レンダリング後のファイルを監査します。
- Compose/K8s のポートマッピング:すべての
ports:/hostPortエントリーについて、どのホストインターフェイスでポートが開くかを確認し、ホストのファイアウォール/SG がそれをカバーしているかを調べます。 - SG の継承:インスタンス → NIC → SG セットの順に確認します。LB/ターゲットグループの SG によって暗黙の許可経路が追加されるため、ポートが閉じていると判断する前にチェーン全体をたどります。
- Docker DNAT:公開ポートは INPUT の設定に関係なく FORWARD 経路を汚染します。DOCKER-USER で制限されていない限り、
-p <pub>:<pub>は自動的に検出事項として扱います。
シンクの分類: インターネット到達可能(パブリックインターフェイス、::/0 SG)、パートナー/VPC 共有、踏み台経由のみ、ループバック。深刻度はシンクに応じて決まります。汚染された経路がインターネット向けシンクに到達する場合は、Severity Assessment に従って Critical/High となります。同じ設定ミスでも、踏み台経由のみのシンクに到達する場合は Medium です。
リポジトリ全体を対象とする grep の実行例は Verification & Validation にあります。ライブ環境の検査ができない場合でも、実行してください。
エクスプロイトと再現
読み取り専用のデモです。認可で指定された位置からのみサービスに接続してください。それ以外の場合は、ホスト内の証拠の解釈に頼ってください。
1. 認証なしでワイルドカードにバインドされた Redis
外部からの実証(認可された接続元からのみ):
$ redis-cli -h 198.51.100.10 ping
PONG # no AUTH required -> unauthenticated reachability proven
$ redis-cli -h 198.51.100.10 CONFIG GET dir
1) "dir"
2) "/var/lib/redis" # writable data dir confirmed -> RCE chain viable
外部テストが認可されていない場合は、ホストの状態だけで同じ事実を実証できます。ss -tlnp の 0.0.0.0:6379 ... redis-server という行が、ワイルドカードへのバインドを示しています。空のファイアウォール、またはファイアウォールが存在しない状態(iptables -S がすべて ACCEPT、または nft list tables が空)と、0.0.0.0/0 の SG ルールによって、インターネットとそのバインドの間に何もないことが分かります。この組み合わせは、外部からのプローブと論理的に同等です。
攻撃経路の説明(典型的な連鎖 — 検出結果に記載し、実行しないでください):認証なしの Redis にアクセス可能 → 攻撃者が CONFIG SET dir /var/spool/cron を実行し、キーとして cron ペイロードを書き込み、SAVE を実行 → Redis がそのキーを root の crontab ファイルにシリアライズ → cron が毎分実行 → root としてリバースシェルを起動します。各手順は、実際の大規模侵害キャンペーンに関する公開レポートに記載されています。
2. ファイアウォールが空であることの証明
開放状態のホストで実行した iptables -L -n -v [ROOT] は、次のとおり、ポリシーが ACCEPT、ルールがゼロで、カウンターだけが表示される正確な形式になります。
# VULNERABLE
Chain INPUT (policy ACCEPT 15432 packets, 892134 bytes)
pkts bytes target prot opt in out source destination
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 12098 packets, 745112 bytes)
pkts bytes target prot opt in out source destination
ufw status verbose で Status: inactive と表示されることを確認し、さらに systemctl is-active ufw でも inactive と表示されることを照合してください。インストール済みで無効になっている管理ツールは、見せかけとしてよく使われます。
3. IPv6 の抜け穴の証明
# VULNERABLE shape
$ iptables -S INPUT | head -3
-P INPUT DROP
-A INPUT -i lo -j ACCEPT
# ...remaining v4 allow rules (established/related, admin ssh, 80/443)...
$ ip6tables -S INPUT
-P INPUT ACCEPT # <- entire v6 plane open
$ ss -tlnp | grep ':::'
LISTEN 0 511 [::]:6379 [::]:* users:(("redis-server",pid=812,fd=7))
説明:攻撃者がホストの AAAA レコードを解決し、IPv6 経由で接続すると、IPv4 のフィルタリングで隠されているはずのサービスにそのまま到達します。アドレスファミリーが ip のルールセットしかないデュアルスタックホストでは、[::] リスナーすべてが IPv6 で完全に露出します。
4. Docker のバイパスを示すデモ
# Evidence chain [ROOT]
$ docker ps --format '{{.Names}}\t{{.Ports}}'
cache 0.0.0.0:6379->6379/tcp # published wildcard
$ ufw status verbose | grep 'deny\|Default'
Default: deny (incoming)
$ iptables -t nat -S DOCKER | grep 6379
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 6379 -j DNAT --to-destination 172.17.0.2:6379
解釈:受信した :6379 のトラフィックは nat PREROUTING で DNAT され、FORWARD の docker チェーン経由で受け入れられます。そのため、ufw の受信拒否 INPUT ポリシーでは評価されません。「ファイアウォールがすべて拒否している」にもかかわらず、コンテナーはインターネットから到達可能です。攻撃経路は上記と同じ認証なし Redis の連鎖ですが、ufw が保護していると信じている運用者には見えません。
修正
ロックアウトの危険 — リモートで適用する前にお読みください。 SSH 経由で制限的なルールセットを適用する前に、帯域外アクセスが利用可能で、正常に機能することを確認してください。ブラウザーのタブで開いたクラウドシリアルコンソールのセッション、アクセス可能な IPMI/iDRAC/iLO、またはハイパーバイザーコンソールを用意します。その後、適用前に自動ロールバックをスケジュールしてください。
echo 'nft flush ruleset && nft -f /etc/nftables.conf' | at now +5 minutes atq # ジョブが登録されていることを確認 # ... 新しいルールセットを適用し、2つ目のシェルからテスト ... atrm <jobid> # アクセスを確認してからロールバックをキャンセル
atdが必要です。systemctl is-active atdで確認し、存在しない場合はディストリビューションのパッケージマネージャー(apt install at/dnf install at)でインストールするか、後で削除する root の crontab のワンライナーを使用してください。atも cron も利用できない場合は、SSH セッションを2つ開いたままにし、一方から適用して、もう一方から確認してから最初のセッションを閉じ、最後の手段として OOB コンソールを利用してください。firewalld の場合は、ランタイムのみの変更(--permanentを省略)を適用すると、systemctl restart firewalldで元に戻ります。ufw の場合は、最初にufw --dry-run enableで確認してください(ルールが表示されるだけで、適用はされません)。
強化された nftables ルールセット(/etc/nftables.conf)
# FIXED — complete reference ruleset, app server, dual stack
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain logdrop {
limit rate 5/second burst 20 packets log prefix "nft-drop " level warn
counter drop
}
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" accept
ct state invalid drop
ct state established,related accept
ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
icmp type echo-request limit rate 5/second accept
meta l4proto ipv6-icmp accept
ip saddr 203.0.113.0/24 tcp dport 22 ct state new meter ssh-brute { ip saddr limit rate over 15/minute } drop
ip saddr 203.0.113.0/24 tcp dport 22 accept
tcp dport { 80, 443 } ct state new limit rate 50/second burst 100 packets accept
jump logdrop
}
chain forward {
type filter hook forward priority 0; policy drop;
# docker-managed forwarding may require explicit allows here; see Docker subsection
}
chain output {
type filter hook output priority 0; policy accept;
}
}
適用して永続化します。
echo 'nft flush ruleset && nft -f /etc/nftables.conf' | at now +5 minutes # rollback guard
nft -f /etc/nftables.conf
nft list ruleset # verify loaded
systemctl is-enabled nftables || systemctl enable nftables
systemctl cat nftables.service # ExecStart must be 'nft -f /etc/nftables.conf'
atrm <jobid>
従来の iptables での同等設定(nft がないホスト向け):-P INPUT DROP、-A INPUT -i lo -j ACCEPT、ESTABLISHED および RELATED の conntrack を受け入れ、CIDR を指定した dport 22、SSH スロットリング用の hashlimit / recent、さらに -m limit --limit 5/s -j LOG --log-prefix "ipt-drop " に続いて -j DROP を指定する LOGDROP チェーンで同じロジックを実装します。netfilter-persistent save で永続化します。
ufw での同等設定
# FIXED — run from console-backed session; dry-run first
ufw --dry-run enable
ufw default deny incoming
ufw default allow outgoing
ufw allow from 203.0.113.0/24 to any port 22 proto tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw logging on
ufw enable
ufw status verbose numbered
ルールが IPv6 にも適用されるように、/etc/default/ufw に IPV6=yes が含まれていることを確認してください(デフォルト値)。ロールバック:ufw disable。
firewalld での同等設定
# FIXED — runtime-only first (self-reverting via restart), then permanent
firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="203.0.113.0/24" service name="ssh" accept'
firewall-cmd --zone=public --remove-service=ssh
firewall-cmd --zone=public --add-service=http --add-service=https
firewall-cmd --set-log-denied=all
# verify SSH still works from admin range, then persist:
firewall-cmd --runtime-to-permanent
firewall-cmd --reload
firewall-cmd --list-all
fail2ban ジェイル
/etc/fail2ban/jail.local:
# FIXED
[DEFAULT]
banaction = nftables-multiport
findtime = 10m
bantime = 1h
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
[sshd]
enabled = true
port = 22
backend = systemd
[nginx-auth]
enabled = true
port = http,https
filter = nginx-auth
logpath = /var/log/nginx/error.log
maxretry = 5
有効化します:systemctl enable --now fail2ban && fail2ban-client status sshd。標準の nginx-auth フィルターは、HTTP ベーシック認証の失敗に一致します。認証情報の詰め込み攻撃も検出したい場合は、継続的な 401/403 応答を対象に nginx アクセスログを正規表現で検索するカスタムフィルターを追加してください。最近の fail2ban では、禁止期間を段階的に延長する bantime.increment = true をサポートしています。
アウトバウンド通信の段階的な展開
率直に言うと、厳格なアウトバウンド通信制限は、一度に導入すると得られる効果以上に多くのものを壊します。DNS リゾルバー、apt ミラー、NTP、監視用のアウトバウンド通信、CDN ベースの API をすべて洗い出す必要があります。段階的に進めてください。
- フェーズ 0 — 観察(リスクなし)。
policy acceptを維持し、明示的な許可ルールがまだない通信をカウント/ログに記録するルールを追加します。# FIXED — nft output additions, observe-only oifname "lo" counter accept ip daddr 203.0.113.53 udp dport 53 counter accept # resolver ip daddr 203.0.113.53 tcp dport 53 counter accept udp dport 123 counter accept # NTP tcp dport { 80, 443 } counter accept # mirrors/APIs, narrow later by CIDR where feasible counter log prefix "egress-would-drop " # watch this for days before Phase 1 - フェーズ 1 — 適用。 output の
policy dropに切り替え、同じ許可ルールを維持し、残りの通信をレート制限付きの egress logdrop チェーンに送ります。 - フェーズ 2 — 絞り込み。 パッケージミラーと既知の API 接続先を、特定の CIDR/FQDN 解決済みセットに固定します。業務上可能な場合は、広範な 80/443 の許可を削除します。
- 必ず含める項目:established/related の許可、ループバック、指定したリゾルバーのみに対する DNS、および
ip daddr 169.254.169.254 dropを適用するかどうかの明示的な判断(メタデータが不要なワークロードではブロックします。クラウドエージェントや cloud-init は必要とする場合があるため、ブロック前に調整してください)。
Docker 連携の修正
主な対策 — compose でループバックにのみ公開します。
# VULNERABLE
ports:
- "6379:6379"
# FIXED
ports:
- "127.0.0.1:6379:6379"
多層防御 — DOCKER-USER で転送トラフィックを制限します。
# FIXED [ROOT] — block off-host reach of sensitive published ports at FORWARD stage
iptables -I DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
iptables -I DOCKER-USER -i eth0 -s 127.0.0.0/8 -j RETURN
iptables -I DOCKER-USER -i eth0 -p tcp -m multiport --dports 3306,5432,6379,27017 -j DROP
netfilter-persistent save # or iptables-save > /etc/iptables/rules.v4; DOCKER-USER survives docker restarts but NOT reboots without persistence
ufw-docker ユーティリティ(github.com/chaifeng/ufw-docker)は、同等の ufw 側 FORWARD ルールを自動化します。手動で管理する DOCKER-USER ルールと比較して評価してください。「修正」として /etc/docker/daemon.json に "iptables": false を設定しないでください。Docker の NAT 管理が無効になり、コンテナーのネットワークが壊れます。
検証と妥当性確認
修正のたびに「確認項目」に記載された露出チェックを再実行してください。各修正で期待される差分を以下に示します。
| 修正 | 検証コマンド | 期待される出力 |
|---|---|---|
| ループバックへの再バインド | ss -tlnp | grep -E '6379|27017|5432' | 127.0.0.1: のプレフィックスのみで、0.0.0.0:/[::]: はない |
| nftables のデフォルト拒否 | nft list ruleset | grep policy | input と forward の policy drop |
| ufw の有効化 | ufw status verbose | Status: active、Default: deny (incoming) |
| firewalld の適用 | firewall-cmd --list-all | ssh サービスがなく、http/https があり、管理用 CIDR のリッチルールがある |
| IPv6 の同等性 | ip6tables -S INPUT | grep '^-A' | wc -l;nft list tables | 0 以外の件数、または inet/ip6 テーブルが存在する |
| fail2ban | fail2ban-client status sshd | Currently banned フィールドを含むジェイルが表示される |
| Docker の封じ込め | iptables -S DOCKER-USER;docker ps --format '{{.Ports}}' | DROP ルールがあり、公開設定はループバックのプレフィックス付きのみ |
否定テスト(正規のトラフィックが引き続き通過する必要があります — 許可された接続元から実行してください):
curl -sSI --max-time 10 https://YOUR-HOST | head -1 # HTTP/1.1 200 or 301 -> web path intact
ssh -o ConnectTimeout=5 -o BatchMode=yes HOST true # succeeds from admin IP
getent hosts archive.ubuntu.com # DNS resolution works under egress rules
ping -c1 -W3 HOST # v6 NDP/ICMP not over-filtered (dual-stack hosts)
ブロック経路のテスト:外部の許可された接続元から、nc -zv -w3 HOST 6379 と nc -zv -w3 HOST 22 を実行し、タイムアウトまたは拒否されることを確認します。IPv6 経路についても AAAA アドレスに対して繰り返します。
リグレッション監視リスト(既知の障害パターン):
- 厳格なアウトバウンド通信制限にリゾルバーの許可がない → apt/ログに
Temporary failure resolvingが表示され、systemd タイマーが再試行ループに陥ります。対処法:まず、設定済みのリゾルバーへの UDP および TCP 53 を許可します(リゾルバーはresolvectl status//etc/resolv.confで確認できます)。 - ESTABLISHED,RELATED の許可がない → ダウンロードが転送途中で停止し、長時間の SSH セッションがフリーズします。
- DOCKER-USER での過剰に広範な DROP → LB ヘルスチェックが失敗し、コンテナーからインターネットへの呼び出しが失敗します。カウンターを
iptables -L DOCKER-USER -n -vで確認してください。 - ICMPv6 を完全にフィルタリング → IPv6 は一時的に動作した後、停止します(ND キャッシュの有効期限切れ)。断続的に発生する IPv6 のみの障害が兆候です。
- bastion の範囲を
ignoreipに指定せずに fail2ban を使用 → 共有 NAT からの一斉接続時に、オペレーターが禁止されます。
構成をコードで管理する際の grep レシピ:
grep -RInE '"0\.0\.0\.0"|0\.0\.0\.0/0|::/0|--host[= ]0\.0\.0\.0|listen_addresses\s*=\s*.{0,3}\*' .
grep -RInE 'bind[- ]address|bindIp|network.host' --include='*.cnf' --include='*.conf' --include='*.yml' . | grep -vE '127\.0\.0\.1'
grep -RInE 'ports:\s*$' --include='docker-compose*.yml' -A2 . | grep -E '[0-9]+:[0-9]+' | grep -v '127\.0\.0\.1:'
grep -RInE 'ufw.*allow.*(22\|3389)\b' . ; grep -RInE 'cidr_blocks?\s*=?.*(0\.0\.0\.0/0)' .
grep -RIlE 'policy accept|-P INPUT ACCEPT' . # ruleset files shipping default-accept
深刻度評価
目安(範囲が変更された派生ケースでは範囲内で調整します。S:C の場合は引き上げます):
| 検出事項 | 深刻度 | CVSS v3.1 ベクター |
|---|---|---|
| 認証なしのサービスがワイルドカードにバインドされ、インターネットから到達可能(Redis/Memcached/MongoDB のような形態) | クリティカル(10.0) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H |
| インターネットから到達可能なホストでファイアウォールが読み込まれていない/すべてのポリシーが ACCEPT | 高(9.3) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L |
| IPv4 はフィルタリングされているが IPv6 は完全に開放されている(または Docker DNAT が INPUT を迂回している) | 高(8.8) | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N |
| 公開されているすべてのホストでアウトバウンドのフィルタリングがない | 中(5.8) | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N |
| デフォルト拒否は設定されているが、破棄ログがない/レート制限がない | 低(3.6) | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N |
補正要素:認証はあるがワイルドカードにバインドされているサービスは、認証が実際の制御となるため、深刻度を1段階下げます。踏み台 CIDR ではなく VPC 全体に管理ポートを開放している場合は、資産の重要度に応じて中~高となります。ワイルドカードバインドがあり、現在はセキュリティグループで閉じている構成は、トポロジーの変更で気付かないうちに有効になるため、高のままです。
よくある誤検知
- Kubernetes ワーカーノード/コントロールプレーンノード — kube-proxy は数千の KUBE-* iptables/nft チェーンをプログラムし、独自の FORWARD の動作を設定します。手動で厳格化するとコントローラーと競合し、サービスのルーティングが壊れます。
iptables -S | grep -c KUBE-または kube-proxy プロセスで検出します。露出状況は代わりに NetworkPolicy/クラウドセキュリティグループのレイヤーで監査してください。 - クラウド LB のヘルスチェック — ALB/GCP のヘルスチェック範囲(例:VPC CIDR、GCP
100.64.0.0/10のプローブ範囲)からターゲットへの到達が必要なため、ホストファイアウォールがこれらの範囲に対して「開放」されていることは、機能上の要件です。検出事項として報告する前に、その背後のリスナーが実際に保護を必要としているか確認し、真に公開されている経路に限定してください。 --network=hostを使用するコンテナー — リスナーは通常のプロセスとしてホストのss -tlnpに表示されます。修正箇所はコンテナーの起動引数/compose であり、ホストのファイアウォールルールも通常どおり適用されます(DNAT なし)。1つの根本原因で両方を説明できる場合、両方のレイヤーを別々の検出事項として二重に報告しないでください。- クラスターの相互接続(Galera、etcd のピアポート、NFS など)は、プライベートインターフェースに広くバインドすることが正当な場合があります。ポートだけではなく、インターフェースの到達可能性に基づいて分類してください。
参考資料
- Netfilter/nftables wiki — https://wiki.nftables.org/ (ルールセットの構文、メーター、ログ);netfilter プロジェクトのドキュメント — https://netfilter.org/documentation/
- マニュアルページ:
nft(8)、iptables(8)、ip6tables(8)、ufw(8)、firewalld(1)、firewall-cmd(1)、fail2ban-client(1)、jail.conf(5)(fail2ban jail 設定マニュアル)、ss(8)、at(1) - Docker の公開ポートと iptables の連携 — https://docs.docker.com/network/iptables/
- ufw-docker ツール — https://github.com/chaifeng/ufw-docker
- fail2ban wiki(フィルター、アクション) — https://github.com/fail2ban/fail2ban/wiki
- CWE-16 Configuration — https://cwe.mitre.org/data/definitions/16.html
- CWE-284 Improper Access Control — https://cwe.mitre.org/data/definitions/284.html
- OWASP Top 10 A05:2021 Security Misconfiguration — https://owasp.org/Top10/A05_2021-Security_Misconfiguration/
- CIS Distribution Independent Linux Benchmark(ファイアウォールのセクションにおけるベースラインの命名)