【実務・中級編】 ネットワークACL(NACL)によるステートレスな防御層の構築 – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

おい、ちょっと手を止めてくれ。先週、あるクライアントのAWS環境で大規模な踏み台化インシデントがあったんだ。

原因を調査したところ、Webサーバーのセキュリティグループ(以下、SG)で 0.0.0.0/0 からの不要なポートがポッカリ空いていた。それだけじゃない。踏み台にされた後、そこを踏み台にして内部の管理用データベース(PostgreSQL)に直接総当たり攻撃を仕掛けられ、ものの見事にデータを抜かれた。

「え、SGで特定のIPだけ許可してたのに、なんで?」って顔をしてるな。
お前らが普段頼り切っているSGはステートフル(状態を保持する)だ。つまり、内側から通信を許可すれば、戻りのパケットは自動的に通ってしまう。これ自体は便利だが、ひとたびアプリケーションに脆弱性(RCEなど)が見つかり、内部からのアウトバウンド通信を許したらどうなる? 攻撃者は意図した外部サーバーへデータを自由に出し入れできるようになる。

ここで登場するのが、今回解説するネットワークACL(NACL)だ。
SGだけを「最後の砦」だと思っているなら、今すぐその甘い考えを捨ててくれ。今回は、現場のインシデントレスキューの視点から、NACLを使った「ステートレスな鉄壁の防御層」の構築方法を叩き込む。

—

1. なぜ「セキュリティグループだけ」では不十分なのか?

多くのジュニアエンジニアは、クラウドのファイアウォールといえばSGを設定して終わりにする。だが、CISSPの観点から言わせてもらえば、SG単体の運用は単一障害点(SPOF)ならぬ「単一防御の罠(Single Point of Failure in Defense)」だ。

ステートフル(SG)とステートレス(NACL)の決定的な違い

  • セキュリティグループ(SG – インスタンスレベル / ステートフル)
  • インバウンド(受信)ルールで許可すれば、アウトバウンド(送信)の戻りパケットは自動的に許可される。
  • 逆に、アウトバウンドのルールを厳しく制限していない場合、内部からの不正なデータ持ち出し(Data Exfiltration)を防ぎきれない。
  • ネットワークACL(NACL – サブネットレベル / ステートレス)
  • サブネットの境界に位置する。
  • インバウンドとアウトバウンドのルールを完全に個別に評価する。つまり、内側から外側へ通信させたい場合でも、アウトバウンドのルールで明示的に許可を書いてやらないとパケットはドロップされる。
  • さらに、NACLには「明示的な拒否(Deny)ルール」が書ける。特定の攻撃者IPレンジ(脅威インテリジェンスで判明したC2サーバー等)を、パケットがインスタンスに到達する前の「サブネットの玄関口」で完全かつ強制的に叩き落とせるのだ。

—

2. 現場で即座に使える! セキュアなNACL設計パターン

実務でパブリックサブネット(Web層)とプライベートサブネット(DB層)を構築する際、デフォルトのNACL(全て許可)をそのまま放置している現場をよく見かける。あれは「鍵の壊れた玄関ドアを開けっ放しにしているようなもの」だ。

ここでは、Web層サブネットに対する「実戦投入可能なNACLのルール構成」を提示する。

Web層サブネット用 NACL ルール定義

インバウンドルール(Inbound Rules)

ルールの番号(Rule #)が若いものから順番に評価される点に注意しろ。評価された瞬間にマッチすれば、その後のルールは無視される(First-match)。

| ルール番号 | プロトコル | ポート範囲 | 送信元 (CIDR) | アクション | 説明 |
| :— | :— | :— | :— | :— | :— |
| 100 | TCP | 80 | 0.0.0.0/0 | ALLOW | HTTPトラフィックの許可 |
| 110 | TCP | 443 | 0.0.0.0/0 | ALLOW | HTTPSトラフィックの許可 |
| 120 | TCP | 1024-65535 | 0.0.0.0/0 | ALLOW | エフェメラルポート(応答用)の受け入れ |
| 190 | ALL | ALL | 悪意あるIP/32 | DENY | 既知の攻撃者IPの即座の遮断 |
| 200 | ALL | ALL | 0.0.0.0/0 | DENY | 明示的なデフォルト拒否(安全網) |

アウトバウンドルール(Outbound Rules)

NACLはステートレスなので、Webサーバーが外部APIを叩いたり、パッチを当てたりするための通信を戻してやる必要がある。

| ルール番号 | プロトコル | ポート範囲 | 送信先 (CIDR) | アクション | 説明 |
| :— | :— | :— | :— | :— | :— |
| 100 | TCP | 80 / 443 | 0.0.0.0/0 | ALLOW | 外部API通信・OSアップデート用 |
| 110 | TCP | 1024-65535 | 0.0.0.0/0 | ALLOW | エフェメラルポートの返送許可 |
| 200 | ALL | ALL | 0.0.0.0/0 | DENY | 明示的なデフォルト拒否 |

—

3. インフラ自動化のためのIaC実装例(Terraform)

口で言うだけなら誰でもできる。これをチームの開発フローに組み込むために、Terraformを使ったセキュアなNACLの実装コードを置いておく。コピペしてそのままモジュールに組み込めるようにコメントも入れておいた。

# =====================================================================
# セキュアなWebサブネット用 NACLの定義
# =====================================================================

resource "aws_network_acl" "web_nacl" {
  vpc_id     = var.vpc_id
  subnet_ids = [aws_subnet.public_web.id]

  # ----------------------------------------------------------------5
  # インバウンドルール(受信)
  # ----------------------------------------------------------------5

  # HTTPの許可
  ingress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 80
    to_port    = 80
  }

  # HTTPSの許可
  ingress {
    protocol   = "tcp"
    rule_no    = 110
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 443
    to_port    = 443
  }

  # 戻り通信(エフェメラルポート)の許可:クライアントからのリクエストに対するレスポンス用
  ingress {
    protocol   = "tcp"
    rule_no    = 120
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 1024
    to_port    = 65535
  }

  # 特定の攻撃元IPレンジからの通信を強制拒否(例: 脅威インテリジェンスで特定されたIP)
  ingress {
    protocol   = "tcp"
    rule_no    = 190
    action     = "deny"
    cidr_block = "203.0.113.50/32" # 実際の脅威IPに置き換えてください
    from_port  = 0
    to_port    = 65535
  }

  # ----------------------------------------------------------------5
  # アウトバウンドルール(送信)
  # ステートレスなため、外向きの通信も明示的に許可が必要
  # ----------------------------------------------------------------5

  # 外部APIへのHTTP/HTTPSリクエストを許可
  egress {
    protocol   = "tcp"
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 80
    to_port    = 80
  }

  egress {
    protocol   = "tcp"
    rule_no    = 110
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 443
    to_port    = 443
  }

  # レスポンス返却用のエフェメラルポート許可
  egress {
    protocol   = "定时或" # (冗談だ、気にしないでくれ。しっかりと実務用のポート範囲を書く)
    protocol   = "tcp"
    rule_no    = 120
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 1024
    to_port    = 65535
  }

  tags = {
    Name        = "secure-web-nacl"
    Environment = "production"
    ManagedBy   = "Terraform"
  }
}

—

4. 現場のプロが教える「NACL運用の落とし穴」とトラブルシューティング

最後に、実際に現場でNACLを導入したエンジニアがやりがちな「痛いミス」を共有しておく。これを知らないと、深夜の緊急メンテで冷や汗をかくことになるぞ。

1. エフェメラルポートの考慮漏れ

  • 一番多いミスがこれだ。NACLはステートレスなので、Webサーバーから外部のDBや外部API(例: StripeやAWSのマネージドサービス)に通信を飛ばした際、その「戻りのパケット」を受け取るためのポート(通常は1024番から65535番のTCPポート)をアウトバウンド、あるいはインバウンドのルールに入れ忘れて接続タイムアウトを起こす。ルール番号の隙間に必ずエフェメラルポートの許可レンジを入れておけ。

2. ルール番号(Rule #)の管理ミスマッチ

  • Terraformなどで後からルールを追加する際、既存のルール番号の間に割って入るのを忘れて、 deny ルールが allow ルールより若い番号になってしまい、正当なトラフィックまで全部遮断したという笑えない事故がある。ルールの順序と番号の間隔(10刻みや20刻みにするなど)は常に美しく保て。

3. SGとの役割分担の混同

  • 「NACLがあるからSGは適当でいいや」は絶対にNGだ。NACLはサブネット単位、SGはインスタンス(またはENI)単位。多層防御(Defense in Depth)の基本は、異なるレイヤーで二重・三重にフィルターをかけることにある。外側のNACLで大雑把な悪意あるIPを弾き、内側のSGで厳密なポート制御を行う。このコンビネーションが崩れた瞬間にセキュリティホールが生まれる。

セキュリティとは、派手なハッキング手法を防ぐことではなく、こうした「地味な設定の積み重ねと整合性の維持」の泥臭い作業そのものだ。
今日の帰りにでも、今お前らが担当しているプロジェクトのNACLがデフォルト(全部許可)になっていないか、確認してみるんだな。もしなっていたら……明日、上長からお叱りを受ける前にこっそり修正しておきなさい。頼んだぞ。

コメント

タイトルとURLをコピーしました