コンテンツにスキップ

ネットワーク設計

ひとことで

パケットがサーバーに届くまでには 3 つの関門があります:ルーティング(経路があるか)→ セキュリティ・リスト / NSG(クラウドのファイアウォールが通すか)→ OS のファイアウォール。 設計は「サーバーにパブリック IP を持たせない」から始めます:入口は LB だけ、運用は要塞(Bastion)経由、出口は用途ごとに分けます。

このページは Phase 1、Phase 2、Phase 4、Phase 9 の実験をまとめたものです。OKE 固有のネットワーク(Pod ネットワーク、API エンドポイント)は Kubernetes のページで扱います。

1. まず構成要素を知る

OCI が初めてなら、AWS と対応づけて理解するのが近道です。名前は違っても、考え方はほぼ同じです。

OCI AWS ひとことで
VCN VPC 自分専用のプライベート・ネットワーク。最初に大きな CIDR を決める(例:10.4.0.0/16)
サブネット(Subnet) Subnet VCN を切り分けた範囲。OCI のサブネットはデフォルトでリージョン全体にまたがる(AWS は AZ ごと)
パブリック / プライベート・サブネット 同左 OCI は prohibit_public_ip_on_vnic で区別:プライベート・サブネットの VM はパブリック IP を持てない
ルート表(Route Table) Route Table 「どの宛先 → どのゲートウェイに渡すか」。サブネットごとに 1 つ関連付ける
インターネット・ゲートウェイ(IGW) Internet Gateway Internet と双方向に通信する。パブリック・サブネットだけが使う
NAT ゲートウェイ NAT Gateway 送信のみ、受信不可:プライベートな VM は外部からダウンロードできるが、外からは接続できない
サービス・ゲートウェイ(SGW) VPC Endpoint Internet を経由せず、Oracle の内部ネットワーク経由で OCI のサービス(オブジェクト・ストレージ、ADB、dnf リポジトリ…)にアクセスする
セキュリティ・リスト(SL) NACL(ただし OCI はデフォルトで stateful) サブネット単位のファイアウォール。サブネット内のすべての VM に効く
ネットワーク・セキュリティ・グループ(NSG) Security Group VNIC(ネットワーク・カード)単位のファイアウォール。その NSG に入れた VM にだけ効く
要塞(Bastion) SSM Session Manager(近い) マネージドな踏み台。パブリック IP のない VM に外からログインする

2. 原理:パケットが通る 3 つの関門

flowchart TB
  P["パケット"] --> R{"① ルート表<br/>経路はある?"}
  R -->|ない| D1["破棄<br/>フロー・ログにも出ない"]
  R -->|ある| S{"② セキュリティ・リスト ∪ NSG<br/>どちらかが許可?"}
  S -->|どちらも不許可| D2["拒否<br/>フロー・ログ:REJECT"]
  S -->|許可| F{"③ OS のファイアウォール<br/>ポートは開いている?"}
  F -->|閉じている| D3["拒否"]
  F -->|開いている| OK["プロセスに到達"]

① 「届くかどうか」はルーティング、「入れるかどうか」はセキュリティ・ルールが決めます。両者は独立しています。 セキュリティ・ルールをすべて許可しても、経路がなければ通じません。経路があっても、セキュリティ・ルールが許可しなければ通じません。

② セキュリティ・リストと NSG は「和集合」です。 どちらか一方が許可すれば通ります。「両方で許可が必要」ではありません。

  • 結果:NSG で細かく絞りたいポートを、SL で広く許可してはいけません。 SL が VCN 全体からの 22 を許可していると、NSG にいくら「要塞だけ許可」と書いても意味がありません
  • AWS は逆です:NACL と Security Group は「両方で許可が必要」(積集合)。AWS から来た人が特につまずきやすいポイントです

③ クラウドで許可しても、OS にもう 1 つ関門があります。 Oracle Linux はデフォルトで firewalld が有効なので、NSG で 80 を許可しても、OS 側で開けなければつながりません。

補足のルール 2 つ:

  • stateful:OCI のルールはデフォルトで stateful です。入ってきた接続の戻りパケットは自動的に許可され、別にルールを書く必要はありません。ただし「送信 / 受信」はそれぞれのネットワーク・カードの立場で判断されます。例:要塞のプライベート・エンドポイントも VM と同じサブネットにあり、そこから VM:22 への通信はエンドポイントにとって「送信」です。SL の送信ルールでも許可が必要です
  • 最長プレフィックス一致:1 つのルート表に 0.0.0.0/0 → NAT と Oracle サービス → SGW が両方あるとき、Oracle サービス向けの通信はより具体的なほうに一致し、SGW を通ります

3. 出口:3 種類のゲートウェイの選び方

要件 使うもの 例
外から接続できる必要がある インターネット・ゲートウェイ(IGW、パブリック・サブネットだけ) LB を置くサブネット
VM が外部からダウンロードしたいが、外から接続されたくない NAT ゲートウェイ(NAT GW) pip パッケージのインストール、Docker Hub のイメージ取得
VM が OCI 自身のサービスにだけアクセスする サービス・ゲートウェイ(SGW) dnf リポジトリ、ボールト、オブジェクト・ストレージ、ADB、監視エージェント
# プライベート・サブネットのルート表(Phase 4、簡略化:実際のコードは dynamic ブロックでスイッチに応じて生成):2 つのルールで、それぞれ別のゲートウェイへ
route_rules {
  destination       = "0.0.0.0/0"
  network_entity_id = oci_core_nat_gateway.this[0].id      # 一般の Internet → NAT
}
route_rules {
  destination       = data.oci_core_services.all[0].services[0].cidr_block  # "all-kix-services-in-oracle-services-network"
  destination_type  = "SERVICE_CIDR_BLOCK"                 # 宛先は「サービス」。IP 範囲は OCI が管理
  network_entity_id = oci_core_service_gateway.this[0].id
}

いちばん安全な組み合わせ:サービス・ゲートウェイ(SGW)だけ

Phase 9 のプライベート VM にはサービス・ゲートウェイしかなく、インターネット・ゲートウェイも NAT ゲートウェイもありません。dnf でのインストール、ボールトからのパスワード取得、エージェントの報告はすべて正常ですが、パブリック・インターネットにはまったく出られません。Internet が不要なサーバーには NAT ゲートウェイを付けません。

4. アクセス制御:セキュリティ・リスト(SL)と NSG の役割分担

セキュリティ・リスト NSG
効く範囲 サブネット内のすべての VM その NSG に入れた VM
ルールのソース / 宛先 CIDR だけ CIDR、または別の NSG
向いているもの サブネット全体で必要な基本ルール 「層」(LB 層、app 層)ごとの業務ルール

このリポジトリのやり方:SL には最低限のものだけを置き、業務ルールはすべて NSG に書き、NSG どうしで参照し合います。

# app 層は「LB 層から」の 80 だけを受け付ける(Phase 4 security.tf)
resource "oci_core_network_security_group_security_rule" "app_http_from_lb" {
  network_security_group_id = oci_core_network_security_group.app.id
  direction                 = "INGRESS"
  protocol                  = "6"                                     # TCP
  source                    = oci_core_network_security_group.lb.id  # ソースは IP ではなく NSG
  source_type               = "NETWORK_SECURITY_GROUP"
  tcp_options {
    destination_port_range {
      min = 80
      max = 80
    }
  }
}

ソースを「lb-nsg」にしておけば、LB の IP が変わっても、app VM が増減しても、ルールの修正は不要です。

SL に残すのは次の 2 種類だけです:

  • ICMP type 3 code 4(Path MTU Discovery):経路の途中に MTU の小さい区間があるとき、この ICMP で送信元にパケットを小さくするよう知らせます。ブロックすると大きなパケットが黙って捨てられます。OCI のデフォルトの SL にもこのルールがあります
  • 送信はすべて許可(送信の制御はルーティングに任せる:NAT を付けなければ外に出られない)

実際にハマった点:SL が VCN 全体からの 22 を許可していた

Phase 4 では当初、SL で「VCN 内 → 22」を許可し、NSG には「22 は要塞からだけ」と書いて、SSH は要塞からしか入れないと考えていました。 和集合なので、実際には VCN 内のどこからでも 22 に接続できました(例:app-1 → app-2)。当時は「要塞からつながる」ことだけを確認し、「ほかからはつながらない」ことを確認しなかったため、気づきませんでした。 現在のコードは SL で 22 を許可せず、SSH は NSG だけで制御するように修正済みです(修正後の再デプロイ検証は未実施)。

5. 運用:パブリック IP のないサーバーにログインするには

方法 やり方 使った場所 評価
パブリック IP + SSH SL / NSG でローカル PC の IP からの 22 を許可 — 利用中のネットワークが 22 をブロックしていて使えない。そもそも攻撃対象になる
Cloud Shell のプライベート・ネットワーク Cloud Shell を VCN のサブネットに接続し、プライベート IP を得てから直接 SSH Phase 2、Phase 6 手軽。ただし Cloud Shell がサブネットにネットワーク・カードを 1 つ残すので、destroy 前に Public に戻す必要がある
OCI 要塞 Cloud Shell → 要塞(セッション)→ VM:22 Phase 4、Phase 9 おすすめ。無料、マネージド(踏み台を自分で保守しない)、セッションに時間制限があり、監査記録も残る
flowchart TB
  CS["Cloud Shell"] -->|"① IAM 権限で<br/>セッション作成<br/>SSH 公開鍵を登録<br/>最長 3 時間"| B["要塞"]
  B -->|"② プライベート・<br/>エンドポイント経由<br/>VCN 内のプライベート IP"| VM["VM :22<br/>NSG は<br/>エンドポイントの IP<br/>だけ許可"]

設計のポイント:

  • NSG の SSH ソースは IP を直書きせず、要塞の属性を参照します:source = "${oci_bastion_bastion.this.private_endpoint_ip_address}/32"
  • セッションは Terraform で管理しません:最長 3 時間で期限切れになるので、管理下に置くと毎回 plan で差分が出ます。使うときに Cloud Shell で作成します
  • 要塞の client_cidr_block_allow_list は 0.0.0.0/0 にするしかありません:Cloud Shell の送信元 IP が固定されないためです。これは意識したうえでのトレードオフで、セキュリティは 2 つのもので担保します:セッションの作成には IAM 権限が必要、接続にはセッションに登録した秘密鍵が必要

Cloud Shell のパブリック IP を直接許可しないのはなぜ?

Cloud Shell の送信元 IP は Oracle の共用 IP で、しかも変わります。これを許可すると、ほかの OCI ユーザーにも開放することになります。

6. 設計判断の一覧

判断 採用 不採用 理由
サーバーの置き場所 プライベート・サブネット、パブリック IP なし パブリック・サブネット + パブリック IP パブリック IP + 開いたポート = 攻撃対象
外部からの入口 LB だけ(パブリック・サブネット) VM を直接公開 入口を 1 か所に集約し、VM は増減・入れ替えできる
セキュリティ・ルールの書き方 NSG を層ごとに作り、相互参照 ルールに IP を書く IP や台数が変わってもルールの修正が不要
SSH のソース制御 要塞の IP を NSG にだけ書く SL で VCN 全体を許可 SL と NSG は和集合なので、SL が広いと NSG が無意味になる
運用経路 OCI 要塞 踏み台 VM を自作 無料、マネージド、パッチ不要、監査記録あり
出口 用途別:NAT + SGW。Internet 不要なら SGW だけ すべて NAT 経由 OCI のサービスへは Internet を経由しない。開けるものは最小限に
Always Free の ADB へのアクセス パブリック・エンドポイント + ACL(自分の VCN とローカル PC の IP だけ許可)、VCN 内からは SGW 経由 プライベート・エンドポイント Always Free の ADB はプライベート・エンドポイント非対応

7. 検証:設計が効いていることをどう証明するか

「つながる」ことの確認だけでは足りません。「つながってはいけないものが本当につながらない」ことも実際に試します。

証明したいこと 正しい方法 証拠にならないもの
Internet への送信が NAT 経由 VM で curl ifconfig.me → NAT ゲートウェイのパブリック IP が表示される dnf install の成功(dnf リポジトリは Oracle サービスで、SGW を通る)
パブリック・インターネットにまったく出られない curl -m 5 ifconfig.me → タイムアウト フロー・ログに記録があるかどうか(経路のないパケットはフロー・ログに出ない。実験で観察した)
ADB へのアクセスが SGW 経由 ADB で sys_context('USERENV','IP_ADDRESS') を確認 → VM のプライベート IP つながったこと(NAT 経由でもつながる)
SSH は要塞からしか入れない 要塞からログインできる + app-1 → app-2:22 がタイムアウトする(当時はこの逆方向のテストをしておらず、問題を見逃した) 要塞からログインできたことだけ
ルールがコードどおり API / コンソールで実際のルールを見る plan がエラーにならなかったこと

8. ハマりどころ

現象 原因 対策
NSG に「X だけ許可」と書いたのに、ほかからも接続できる SL と NSG は和集合で、SL の許可が広すぎる 細かく制御したいポートは SL で許可しない。逆方向のテストをする
要塞のセッションで VM に接続できない SL が送信 443 しか許可しておらず、エンドポイント → VM:22 はエンドポイントにとって送信 SL に「サブネット内への 22 の送信」を追加
destroy でサブネットがいつまでも削除されない Cloud Shell のプライベート・ネットワークのネットワーク・カードがサブネットに残っている destroy 前に Cloud Shell の Network を Public に戻す。list_private_ips(subnet_id=...) で誰が残っているか調べる
dnf install が成功したので NAT が通じたと思った dnf リポジトリは SGW を通る curl ifconfig.me で送信元 IP を確認
ローカル PC からの SSH がタイムアウト 利用中のネットワークが送信の 22 をブロック Cloud Shell(プライベート・ネットワークまたは要塞)を使う

9. 確認問題

1. VM の SL が 22 を許可し、NSG は 22 を許可していない。VCN 内から SSH できる?

できます。SL と NSG は和集合なので、どちらかが許可すれば通ります。

2. 同じ設定を AWS で(NACL は許可、Security Group は不許可)。接続できる?

できません。AWS の NACL と Security Group は両方で許可が必要です。

3. プライベート・サブネットの VM で dnf install ができた。NAT ゲートウェイが正しく設定されている証拠になる?

なりません。OCI の dnf リポジトリは Oracle サービスなので、SGW があればインストールできます。curl ifconfig.me で送信元 IP が NAT のパブリック IP かどうかを確認します。

4. ボールトからパスワードを取得し、オブジェクト・ストレージにログを書くだけの VM。出口はどう設定する?

SGW だけを付け、NAT と IGW は付けません。OCI のサービスには問題なくアクセスでき、パブリック・インターネットにはまったく出られません。

5. app-nsg の 80 番ポートのソースを、LB の IP ではなく lb-nsg にするのはなぜ?

LB の IP は変わる可能性があり、app VM も増減します。NSG で書けばルールの修正が不要で、ルール自体が「LB 層だけが app 層にアクセスできる」ことを表します。

6. 要塞の client_cidr_block_allow_list が 0.0.0.0/0。安全?

トレードオフです。Cloud Shell の送信元 IP は固定されないため、ソースを絞れません。セッションの作成に IAM 権限が必要、接続にセッションに登録した秘密鍵が必要、セッションは最長 3 時間、という点で安全性を担保しています。クライアントの IP が固定なら絞るべきです。

7. 「SSH は要塞からしか入れない」ことはどう証明する?

両方向を試します:要塞からログインできること(正方向)、VCN 内のほかの VM から 22 への接続がタイムアウトすること(逆方向)。正方向だけでは、SL の許可が広すぎるといった問題に気づけません。

8. ルート表に 0.0.0.0/0 → NAT と Oracle サービス → SGW が両方ある。オブジェクト・ストレージへのアクセスはどちらを通る?

SGW です。ルーティングは最長プレフィックス一致で、Oracle サービスのアドレス範囲のほうが 0.0.0.0/0 より具体的だからです。

コード全文

本文には要点だけを抜粋しました。コード全体(Phase 4 の 3 層構成):

04-3tier/modules/network/main.tf(VCN / ゲートウェイ / ルート表 / SL / サブネット)
# ------------------------------------------------------------
# network module:VCN 一式(Phase 4 版)
#   VCN ─ インターネット・ゲートウェイ / NAT ゲートウェイ / サービス・ゲートウェイ
#    ├─ パブリック・サブネット  ─ rt-public (0.0.0.0/0 → IGW)
#    └─ プライベート・サブネット ─ rt-private(0.0.0.0/0 → NAT GW、Oracle Services → サービス・ゲートウェイ)
#   セキュリティ・リストは全サブネットで 1 つを共用
# 03 との違い:
#   - NAT GW / サービス・ゲートウェイを追加(bool のスイッチで、count で作る / 作らないを制御)
#   - プライベート・サブネットは VCN のデフォルトのルート表を使わず、自作の rt-private を使う
# ------------------------------------------------------------

locals {
  # for_each 用の式:is_public でサブネットを 2 グループに分ける
  public_subnets  = { for k, s in var.subnets : k => s if s.is_public }
  private_subnets = { for k, s in var.subnets : k => s if !s.is_public }

  create_nat = var.enable_nat_gateway && length(local.private_subnets) > 0
  create_sgw = var.enable_service_gateway && length(local.private_subnets) > 0
}

resource "oci_core_vcn" "this" {
  compartment_id = var.compartment_ocid
  cidr_blocks    = [var.vcn_cidr]
  display_name   = "${var.name_prefix}-vcn"
  dns_label      = var.vcn_dns_label

  freeform_tags = var.freeform_tags
}

# ---- ゲートウェイ ----

# パブリック・サブネットが 1 つもなければ IGW は不要 → count で 0 個か 1 個かを制御
resource "oci_core_internet_gateway" "this" {
  count = length(local.public_subnets) > 0 ? 1 : 0

  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${var.name_prefix}-igw"
  enabled        = true

  freeform_tags = var.freeform_tags
}

# NAT GW:プライベート・サブネットの VM は Internet へ「内から外」にだけ接続できる(外からは接続できない)
#   用途:dnf install、OS の更新など
resource "oci_core_nat_gateway" "this" {
  count = local.create_nat ? 1 : 0

  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${var.name_prefix}-natgw"

  freeform_tags = var.freeform_tags
}

# サービス・ゲートウェイの接続先:"All <region> Services In Oracle Services Network"
#   (もう一方の選択肢はオブジェクト・ストレージだけのもの。ADB にはこの All のほうが必要)
data "oci_core_services" "all" {
  count = local.create_sgw ? 1 : 0

  filter {
    name   = "name"
    values = ["All .* Services In Oracle Services Network"]
    regex  = true
  }
}

# サービス・ゲートウェイ:Internet を経由せず、Oracle の内部ネットワークで OCI のサービス(ADB、オブジェクト・ストレージなど)に直接アクセス
resource "oci_core_service_gateway" "this" {
  count = local.create_sgw ? 1 : 0

  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${var.name_prefix}-sgw"

  services {
    service_id = data.oci_core_services.all[0].services[0].id
  }

  freeform_tags = var.freeform_tags
}

# ---- ルート表 ----

resource "oci_core_route_table" "public" {
  count = length(local.public_subnets) > 0 ? 1 : 0

  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${var.name_prefix}-rt-public"

  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_internet_gateway.this[0].id # count で作ったリソースは [0] で参照
  }

  freeform_tags = var.freeform_tags
}

# プライベート用のルート表:NAT / サービス・ゲートウェイのうち有効なものの数だけルールを生成
#   Oracle Services 向けの通信はより具体的な SGW のルールに一致し、NAT は通らない(最長プレフィックス一致)
resource "oci_core_route_table" "private" {
  count = length(local.private_subnets) > 0 ? 1 : 0

  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${var.name_prefix}-rt-private"

  dynamic "route_rules" {
    for_each = local.create_nat ? [1] : []
    content {
      destination       = "0.0.0.0/0"
      destination_type  = "CIDR_BLOCK"
      network_entity_id = oci_core_nat_gateway.this[0].id
    }
  }

  dynamic "route_rules" {
    for_each = local.create_sgw ? [1] : []
    content {
      # 宛先は CIDR ではなく「サービス」(例:all-kix-services-in-oracle-services-network)
      destination       = data.oci_core_services.all[0].services[0].cidr_block
      destination_type  = "SERVICE_CIDR_BLOCK"
      network_entity_id = oci_core_service_gateway.this[0].id
    }
  }

  freeform_tags = var.freeform_tags
}

# ---- セキュリティ・リスト ----

resource "oci_core_security_list" "this" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${var.name_prefix}-sl"

  egress_security_rules {
    destination = "0.0.0.0/0"
    protocol    = "all"
  }

  # dynamic ブロック:var.ssh_allowed_cidrs の数だけルールを生成
  dynamic "ingress_security_rules" {
    for_each = var.ssh_allowed_cidrs
    content {
      source      = ingress_security_rules.value
      protocol    = "6"
      description = "SSH"
      tcp_options {
        min = 22
        max = 22
      }
    }
  }

  # ICMP type 3 code 4(Path MTU Discovery)
  ingress_security_rules {
    source   = "0.0.0.0/0"
    protocol = "1"
    icmp_options {
      type = 3
      code = 4
    }
  }

  freeform_tags = var.freeform_tags
}

# ---- サブネット ----

# for_each:var.subnets のエントリごとにサブネットを 1 つ作成
#   state 上のアドレスは oci_core_subnet.this["public"] のようになる
resource "oci_core_subnet" "this" {
  for_each = var.subnets

  compartment_id    = var.compartment_ocid
  vcn_id            = oci_core_vcn.this.id
  cidr_block        = each.value.cidr
  display_name      = "${var.name_prefix}-subnet-${each.key}"
  dns_label         = each.value.dns_label
  security_list_ids = [oci_core_security_list.this.id]

  route_table_id             = each.value.is_public ? oci_core_route_table.public[0].id : oci_core_route_table.private[0].id
  prohibit_public_ip_on_vnic = !each.value.is_public

  freeform_tags = var.freeform_tags
}
04-3tier/env/security.tf(NSG)
# ------------------------------------------------------------
# ネットワーク・セキュリティ・グループ(NSG):「層」ごとに作成し、同じ層の VM で共用
#   lb-nsg :ロード・バランサ(Load Balancer)
#   app-nsg:app VM ×2
#   ルールの「ソース/宛先」に別の NSG を指定可能(source_type = NETWORK_SECURITY_GROUP)
#   → IP を書かずに済み、VM の増減や IP 変更があってもルールの修正は不要
#
#   my_ip ──:80──→ [lb-nsg] LB ──:80──→ [app-nsg] VM ×2 ←──:22── 要塞(Bastion)
#
# checkov CKV_OCI_21(ingress ルールに stateless を要求)→ あえて対応せず、各ルールの skip コメントに理由を記載
#   stateless = 接続状態を記録しないため、戻りトラフィック用のルールが別途必要(ルール数が倍、設定ミスしやすい)。利点は大量トラフィック時の性能
#   ここはトラフィックが小さいため、デフォルトの stateful を使用
# ------------------------------------------------------------

# ---- lb-nsg ----

resource "oci_core_network_security_group" "lb" {
  compartment_id = var.compartment_ocid
  vcn_id         = module.network.vcn_id
  display_name   = "${local.name_prefix}-lb-nsg"

  freeform_tags = local.common_tags
}

# イングレス:ローカル PC の IP から LB の 80 へのアクセスのみ許可
resource "oci_core_network_security_group_security_rule" "lb_http_from_my_ip" {
  #checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
  network_security_group_id = oci_core_network_security_group.lb.id
  direction                 = "INGRESS"
  protocol                  = "6"
  source                    = "${var.my_ip}/32"
  source_type               = "CIDR_BLOCK"
  description               = "HTTP from my IP"

  tcp_options {
    destination_port_range {
      min = 80
      max = 80
    }
  }
}

# エグレス:LB → app VM の 80(リクエスト転送 + ヘルスチェック)
#   サブネット(Subnet)のセキュリティ・リスト(Security List)で全エグレスを許可済みのため、このルールがなくても通信は可能。NSG 単体で「LB は app にのみ送信」と示すために明記
resource "oci_core_network_security_group_security_rule" "lb_http_to_app" {
  network_security_group_id = oci_core_network_security_group.lb.id
  direction                 = "EGRESS"
  protocol                  = "6"
  destination               = oci_core_network_security_group.app.id
  destination_type          = "NETWORK_SECURITY_GROUP"
  description               = "HTTP to app"

  tcp_options {
    destination_port_range {
      min = 80
      max = 80
    }
  }
}

# ---- app-nsg ----

resource "oci_core_network_security_group" "app" {
  compartment_id = var.compartment_ocid
  vcn_id         = module.network.vcn_id
  display_name   = "${local.name_prefix}-app-nsg"

  freeform_tags = local.common_tags
}

# SSH は要塞からの接続のみ受け付ける
#   要塞は app サブネット内にプライベート・エンドポイント(Private Endpoint、プライベート IP)を持ち、SSH はこの IP から入ってくる
resource "oci_core_network_security_group_security_rule" "app_ssh_from_bastion" {
  #checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
  network_security_group_id = oci_core_network_security_group.app.id
  direction                 = "INGRESS"
  protocol                  = "6"
  source                    = "${oci_bastion_bastion.this.private_endpoint_ip_address}/32"
  source_type               = "CIDR_BLOCK"
  description               = "SSH from Bastion"

  tcp_options {
    destination_port_range {
      min = 22
      max = 22
    }
  }
}

# HTTP は LB からの接続のみ受け付ける(ソースは IP ではなく lb-nsg を指定)
#   app VM 同士、要塞 → VM の 80 は引き続き通信不可
resource "oci_core_network_security_group_security_rule" "app_http_from_lb" {
  #checkov:skip=CKV_OCI_21:Low traffic lab; stateful rules are simpler and sufficient
  network_security_group_id = oci_core_network_security_group.app.id
  direction                 = "INGRESS"
  protocol                  = "6"
  source                    = oci_core_network_security_group.lb.id
  source_type               = "NETWORK_SECURITY_GROUP"
  description               = "HTTP from LB"

  tcp_options {
    destination_port_range {
      min = 80
      max = 80
    }
  }
}
04-3tier/env/bastion.tf
# ------------------------------------------------------------
# OCI 要塞(Bastion)サービス(Always Free)
#   踏み台は OCI がマネージド。VM の作成やパッチ適用は不要
#   Terraform で作るのは要塞本体だけ。セッション(有効期間は最長 3 時間)は使うたびにコンソール / CLI で作成
#   → セッションを Terraform で管理すると、期限切れのたびに plan で差分が出る
# ------------------------------------------------------------

resource "oci_bastion_bastion" "this" {
  compartment_id   = var.compartment_ocid
  bastion_type     = "STANDARD"
  name             = "${local.name_prefix}bastion" # 英数字のみ(- は使えない)
  target_subnet_id = module.network.subnet_ids["app"]

  # 要塞への接続を許可するソース
  #   利用中のネットワークが 22 をブロック → 実際は Cloud Shell から接続。Cloud Shell のパブリック IP は固定されない → 0.0.0.0/0 にするしかない
  #   セキュリティはセッションで担保:セッションの作成には IAM 権限が必要で、セッション作成時に登録した SSH 公開鍵しか受け付けない
  client_cidr_block_allow_list = ["0.0.0.0/0"]

  max_session_ttl_in_seconds = 10800 # 3 時間(上限)

  freeform_tags = local.common_tags
}