0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Azure P2S VPN の構築

0
Posted at

Azure の Point-to-Site (P2S) VPN を Microsoft Entra ID 認証で構築し、Private Endpoint 経由で Storage Account にアクセスする最小構成を Bicep + azd で組んで学習しました。

「Private Endpoint と Private DNS Zone を作れば VPN クライアントから Storage の FQDN が Private IP に解決されるはず」と思って始めたのですが、想像と違う動きにいくつも詰まりました。本記事は、その「想像と違う」ポイントを整理した記録です。

  • 想定読者: Azure の閉域構成や P2S VPN を初めて触る・整理したい Azure / MS エンジニア
  • 得られる知見:
    • P2S VPN を Bicep で IaC 化する最小構成
    • DNS と TCP は別レイヤー であることを実感する設計
    • P2S クライアントは Private DNS Zone を自動解決できない という公式仕様
    • Private Endpoint は ICMP に応答しない 仕様
    • WSL2 + Azure VPN Client の組み合わせで意外と動く件

結論

  • ✅ P2S VPN + AAD 認証 + Private Endpoint + Private DNS Zone を Bicep でフルコード化できた
  • ✅ デプロイ時間は VPN Gateway がほぼ全てで 約 35 分
  • ⚠️ Private DNS Zone を作っても P2S クライアントからは自動解決されない(hosts / DNS Resolver / NRPT が必要)
  • ⚠️ publicNetworkAccess: Disabled でもパブリック DNS には常に Public IP が公開される(DNS と TCP は完全に別レイヤー)
  • ⚠️ Private Endpoint は ICMP 非対応ping のタイムアウトは正常)
  • ⚠️ VPN Gateway は停止不可 で、VpnGw1AZ で月額 $150 以上の時間課金が継続発生。学習用途では azd down 必須

アーキテクチャ

環境

項目 バージョン / 値
Azure CLI 2.x
azd 1.24.x
Bicep 0.41.x
VPN Gateway SKU VpnGw1AZ (Generation2)
クライアント Pool 172.16.0.0/24
リージョン japaneast
Audience ID (Microsoft 登録 Azure VPN Client) c632b3df-fb67-4d84-bdcf-b95ad541b5c8

構築手順

Step 1: VNet と GatewaySubnet

GatewaySubnet は 予約名(変更不可)。サイズは /27 以上推奨。

infra/modules/network.bicep
// IMPORTANT: GatewaySubnet is a RESERVED name. Do not change it.
var gatewaySubnetName = 'GatewaySubnet'
var gatewaySubnetPrefix = '10.10.255.0/27'

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
  name: 'vnet-vpn-learn'
  location: location
  tags: tags
  properties: {
    addressSpace: { addressPrefixes: ['10.10.0.0/16'] }
    subnets: [
      { name: gatewaySubnetName, properties: { addressPrefix: gatewaySubnetPrefix } }
      {
        name: 'snet-private-endpoint'
        properties: {
          addressPrefix: '10.10.1.0/24'
          privateEndpointNetworkPolicies: 'Disabled'
        }
      }
    ]
  }
}

Step 2: VPN Gateway (Entra ID 認証)

ポイントは 3 つ:

  1. SKU は VpnGw1AZ 必須(2025-11-01 以降、非 AZ SKU は新規作成不可)
  2. Generation2 必須(AZ SKU は Gen1 非対応)
  3. OpenVPN + AAD の組み合わせ(IKEv2/SSTP では AAD 認証不可)
infra/modules/vpn-gateway.bicep
// Microsoft が登録済みの Azure VPN Client App。管理者同意は不要。
var azureVpnClientAppId = 'c632b3df-fb67-4d84-bdcf-b95ad541b5c8'
var aadTenantUrl = '${environment().authentication.loginEndpoint}${tenantId}/'
#disable-next-line no-hardcoded-env-urls
var aadIssuerUrl = 'https://sts.windows.net/${tenantId}/'

resource vpnGateway 'Microsoft.Network/virtualNetworkGateways@2023-11-01' = {
  name: 'vpngw-vpn-learn'
  location: location
  properties: {
    gatewayType: 'Vpn'
    vpnType: 'RouteBased'
    vpnGatewayGeneration: 'Generation2'
    sku: { name: 'VpnGw1AZ', tier: 'VpnGw1AZ' }
    ipConfigurations: [/* 省略: Public IP + GatewaySubnet */]
    vpnClientConfiguration: {
      vpnClientAddressPool: { addressPrefixes: ['172.16.0.0/24'] }
      vpnClientProtocols: ['OpenVPN']
      vpnAuthenticationTypes: ['AAD']
      aadTenant: aadTenantUrl
      aadAudience: azureVpnClientAppId
      aadIssuer: aadIssuerUrl
    }
  }
}

Step 3: Storage Account を最初から閉域に

「後から publicNetworkAccessDisabled に切り替える」のではなく、作成時から閉じるのが安全。

infra/modules/storage.bicep
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageAccountName
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
    publicNetworkAccess: 'Disabled'
    networkAcls: { defaultAction: 'Deny', bypass: 'AzureServices' }
  }
}

Step 4: Private Endpoint + Private DNS Zone

Private DNS Zone Group を関連付けると、Azure が A レコードを 自動登録 してくれる(PE の IP が変わっても追従)。

infra/modules/private-endpoint.bicep
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = {
  name: 'privatelink.blob.${environment().suffixes.storage}'
  location: 'global'
}

resource vnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = {
  parent: privateDnsZone
  name: 'vnet-link-vpn-learn'
  location: 'global'
  properties: {
    virtualNetwork: { id: vnetId }
    registrationEnabled: false
  }
}

resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = {
  name: 'pep-<storage-name>-blob'
  location: location
  properties: {
    subnet: { id: privateEndpointSubnetId }
    privateLinkServiceConnections: [
      {
        name: 'plsc-blob'
        properties: {
          privateLinkServiceId: storageAccountId
          groupIds: ['blob']
        }
      }
    ]
  }
}

resource privateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = {
  parent: privateEndpoint
  name: 'default'
  properties: {
    privateDnsZoneConfigs: [
      {
        name: 'blob'
        properties: { privateDnsZoneId: privateDnsZone.id }
      }
    ]
  }
  dependsOn: [vnetLink]
}

Step 5: デプロイ

deploy.sh
cd vpn-gateway
azd auth login
azd env new vpn-learn-001
azd env set AZURE_TENANT_ID <your-tenant-id>
azd up

⏰ VPN Gateway の作成で約 35 分かかります(Storage / PE / VNet は数十秒)。

リソース確認

VNet

Subnet snet-private-endpoint のNSG

DefaultのNSG
image.png

接続デバイス

ここにVPN Gatewayがある
image.png

動かして確認

ステップ 1: VPN クライアント設定パッケージのダウンロード

  1. Azure Portal で 仮想ネットワークゲートウェイ を開く
    • リソースグループ: azd env get-value AZURE_RESOURCE_GROUP_NAME で取得した名前
    • 名前: vpngw-vpn-learn
  2. 左側メニュー → 「ポイント対サイトの構成」 をクリック
  3. 上部の 「VPN クライアントのダウンロード」 をクリック
  4. zip ファイルがダウンロードされる
  5. zip を解凍し、AzureVPN/azurevpnconfig.xml を確認

image.png

ステップ 2: Windows 10 / 11 へのインストール

Azure VPN Client のインストール

  1. Microsoft Store を開く
  2. 「Azure VPN Client」を検索してインストール

設定ファイルのインポート

  1. Azure VPN Client を起動
  2. 左下の 「+」「インポート」 をクリック
  3. ステップ 1 で解凍した AzureVPN/azurevpnconfig.xml を選択
  4. 接続情報が表示されるので 「保存」 をクリック

image.png

接続

  1. Azure VPN Client で接続プロファイルを選択
  2. 「接続」 をクリック
  3. ブラウザに Entra ID のサインイン画面が表示される
  4. サインインすると VPN Client に戻り、「接続済み」になる

ステップ 3: 接続確認

接続後、以下のいずれかで VPN クライアント IP がアサインされていることを確認します。
PowerShellで出力中に以下のような PPP/Tunnel アダプター172.16.0.x の IP が出ていれば成功:

ipconfig

<中略>

PPP アダプター vnet-vpn-learn:

   接続固有の DNS サフィックス . . . . .:
   IPv4 アドレス . . . . . . . . . . . .: 172.16.0.2
   サブネット マスク . . . . . . . . . .: 255.255.255.255
   デフォルト ゲートウェイ . . . . . . .:

Storage Account接続

以下のPowerShell実行でStorage AccountのIPアドレスを取得。

$NIC_ID = az network private-endpoint show -g rg-vpn-learn-001 -n pep-stvpngwuqem7g53eqrtw-blob --query "networkInterfaces[0].id" -o tsv
az network nic show --ids $NIC_ID --query "ipConfigurations[0].privateIPAddress" -o tsv

Hosts に追記。

C:\Windows\System32\drivers\etc\hosts
10.10.1.4   stvpngwuqem7g53eqrtw.blob.core.windows.net

VPN接続状態で、以下のコマンドでPublic IPが返る。DNSは登録されているので問題なし。

nslookup stvpngwuqem7g53eqrtw.blob.core.windows.net

pingは失敗するがIPアドレスはhostsを見れている。pingが届かないのは想定通り。

 ping stvpngwuqem7g53eqrtw.blob.core.windows.net

stvpngwuqem7g53eqrtw.blob.core.windows.net [10.10.1.4] ping を送信しています 32 バイトのデータ:
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。

10.10.1.4  ping 統計:
    パケット数: 送信 = 4、受信 = 0、損失 = 4 (100% の損失)

VPN接続時で、azコマンドなら大丈夫。

> az storage container list `
>>   --account-name stvpngwuqem7g53eqrtw `
>>   --auth-mode login `
>>   --output table
Name    Lease Status    Last Modified
------  --------------  -------------------------
test                    2026-05-16T06:34:19+00:00

VPN切断時だと以下のようなエラー。

 az storage container list `
>>   --account-name stvpngwuqem7g53eqrtw `
>>   --auth-mode login `
>>   --output table
(<HTTPSConnection(host='stvpngwuqem7g53eqrtw.blob.core.windows.net', port=443) at 0x21175477390>, 'Connection to stvpngwuqem7g53eqrtw.blob.core.windows.net timed out. (connect timeout=None)')

VPN接続時だとデータプレーンをPortalから参照可能。

image.png

VPN切断時だとデータプレーンをPortalから参照不可。

image.png

ハマりポイント

publicNetworkAccess: Disabled でも DNS には Public IP が公開される

最初の混乱はこれでした。「Storage を閉域にしたなら DNS も引けなくなるのでは?」と思いきや、nslookup すると堂々と Public IP が返ってきます。

Storage Account の publicNetworkAccess: Disabled は TCP/HTTP レイヤーのファイアウォール設定であり、DNS レコードを消すものではない。

CNAME チェーンを dig +noshort で見るとよく分かります:

dig-output.txt
stvpngw<suffix>.blob.core.windows.net
  → (CNAME) stvpngw<suffix>.privatelink.blob.core.windows.net
  → (CNAME) blob.tyo22prdstr09a.store.core.windows.net
  → (A)     20.60.xxx.xxx   ← Public IP

つまり Azure は どんな Storage Account にも常にパブリック DNS レコードを公開 していて、publicNetworkAccess の設定はあくまで「その IP への TCP 接続を Storage 側で受け付けない」という設定。

クライアント DNS 解決結果 TCP アクセス
インターネット Public IP Storage が拒否 (Disabled)
VNet 内 + Private DNS Zone Private IP PE 経由で成功
P2S クライアント (デフォルト) Public IP Public IP に向かい Storage 拒否
P2S クライアント + hosts Private IP (強制) VPN 経由で PE に成功

privatelink.* CNAME が CNAME チェーンに必ず挟まっているのは、Private DNS Zone を作ったときに同名ゾーンを上書きできるようにするための仕掛け です。これが Private Link の本質。

② P2S クライアントは Private DNS Zone を自動解決できない

ここが最大の罠。「VNet に Private DNS Zone をリンクしたから、VPN 経由でも引けるはず」と思っていたら、引けません

Azure VPN P2S クライアントは、VNet にリンクされた Private DNS Zone を自動では解決できない。

公式根拠:

本番での解決策は通常 Azure DNS Private Resolver の inbound endpoint を VNet に立てて、P2S クライアントの DNS をそこに向ける構成(または DNS フォワーダー VM)。

学習用途では公式ドキュメントにも紹介されている hosts ファイル手動追記 で回避できます。

add-hosts.ps1
# PowerShell 管理者として実行
$PE_IP = az network private-dns record-set a list `
  -g rg-vpn-learn-001 `
  -z privatelink.blob.core.windows.net `
  --query "[0].aRecords[0].ipv4Address" -o tsv

$FQDN = "<storage-name>.blob.core.windows.net"
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "$PE_IP`t$FQDN"

🎓 「Private DNS Zone は作るのに無駄では?」 → そんなことはなく、VNet 内クライアント(VM / Function / Container Apps)からはちゃんと引けます。「誰から引けて誰から引けないか」を理解するのが Private Link 学習の核心。

③ Private Endpoint は ICMP に応答しない

hosts を書いて意気揚々と ping したら全部タイムアウト。慌てます。

Private Endpoint は ICMP 非対応(公式仕様)。ping のタイムアウトは正常動作。

公式根拠: Azure Private Link FAQ - Do private endpoints support ICMP traffic?

"TCP and UDP traffic are only supported for a private endpoint."

疎通確認は TCP 443 で行います:

test-tcp.ps1
Test-NetConnection -ComputerName <storage-name>.blob.core.windows.net -Port 443
# TcpTestSucceeded : True なら OK

Linux / Mac は Bash の /dev/tcp 疑似デバイスが楽:

test-tcp.sh
timeout 5 bash -c "echo > /dev/tcp/<storage-name>.blob.core.windows.net/443 && echo OK || echo NG"

④ Private Endpoint の IP の取り方(customDnsConfigs の罠)

PE の IP を取ろうとして、こう書きたくなります:

wrong.sh
# ❌ これでは IP が取れない(customDnsConfigs が空のことが多い)
az network private-endpoint show -g $RG -n pep-... \
  --query "customDnsConfigs[0].ipAddresses[0]" -o tsv

customDnsConfigs は subResource ごとの追加 FQDN 情報用フィールドで、Storage の blob のような基本 subResource では でした。正しい取り方は 2 通り:

get-pe-ip.sh
# 方法 A: Private DNS Zone の A レコードから(最も楽)
az network private-dns record-set a list \
  -g $RG -z privatelink.blob.core.windows.net \
  --query "[0].aRecords[0].ipv4Address" -o tsv

# 方法 B: PE 経由で NIC リソースから
NIC_ID=$(az network private-endpoint show -g $RG -n pep-... \
  --query "networkInterfaces[0].id" -o tsv)
az network nic show --ids "$NIC_ID" \
  --query "ipConfigurations[0].privateIPAddress" -o tsv

🎓 Private Endpoint は実は「PE リソース + NIC リソース + Private DNS A レコード」の 3 つの組 で構成されている。メインの IP は NIC リソースに格納される。

⑤ WSL2 + Azure VPN Client で意外と動いた話

Azure VPN Client は Linux 非対応なので、WSL2 ユーザーは Windows ホスト側で接続するしかありません。一般的に WSL2 (NAT モード) は VPN ルーティングを引き継がない、と覚えていました。

ところが Windows 11 + WSL2 (Hyper-V firewall + DNS Tunneling) では、WSL からも普通に Private Endpoint に TCP 443 で到達できました

wsl-test.sh
# WSL 内から実行
$ powershell.exe -Command "Get-NetIPAddress -InterfaceAlias 'vnet-vpn-learn'"
IPAddress  : 172.16.0.2   # ← Windows 側で VPN 接続済み

$ timeout 5 bash -c "echo > /dev/tcp/10.10.1.4/443 && echo OK"
OK   # ← WSL から PE に到達できている

しかも dig で Private DNS Zone まで引けてしまうケースがあり、これは Azure VPN Client が NRPT (Name Resolution Policy Table) を使う ためと推測されます。

Microsoft 公式 Azure VPN Client docs より:

"When using Microsoft Entra ID authentication, the Azure VPN Client utilizes DNS Name Resolution Policy Table (NRPT) entries"

NRPT は 特定ドメインだけ別 DNS に転送する Windows 機能。Win11 22H2 以降の WSL2 DNS Tunneling と組み合わさると、結果として WSL からも Private DNS Zone が引けてしまうことがある。

つまり「P2S では Private DNS Zone を引けない」は デフォルト構成限定 の話で、Azure VPN Client + NRPT + 適切な DNS サーバー指定があれば引けます。

検証結果

シナリオ 結果 備考
Bicep az bicep build warnings 0
azd up 全体 約 35 分 (VPN GW がほぼ全て)
Azure VPN Client サインイン Entra ID + OpenVPN
接続後 nslookup (デフォルト構成) ⚠️ Public IP が返る 想定通り(DNS 制限)
hosts 追加後 nslookup ✅ Private IP 10.10.1.4 回避策成功
ping (ICMP) ❌ タイムアウト 仕様通り(ICMP 非対応)
Test-NetConnection -Port 443 ✅ True TCP 疎通 OK
az storage container list (接続中) ✅ コンテナ表示 PE 経由で成功
az storage container list (切断後) ❌ タイムアウト publicNetworkAccess Disabled の効果
azd down --purge --force ✅ 全削除 約 15 分

まとめ

学び

  • Private Link は DNS と TCP を別々に設計 する必要がある
  • Private DNS Zone は VNet 内クライアント用 であり、P2S クライアントは別途仕組みが必要
  • Private Endpoint は L4 (TCP/UDP) のみ、ICMP は仕様外
  • VPN Gateway は 時間課金が止められないので、学習後は必ず削除(azd down

限界・残課題

  • 本記事では DNS Private Resolver を使わず hosts で回避した。次は Resolver を Bicep で組んで「本番相当」を試したい
  • VPN Gateway デプロイの 35 分は短縮不可。学習用としては大きな心理的コスト
  • Azure VPN Client の Linux サポートが無いのは継続的な不便。Microsoft の Public Roadmap で進展を期待

次にやりたいこと

  • Azure DNS Private Resolver を追加して「P2S → Private DNS Zone」を正規ルートで通す
  • VNet Peering を組んで複数 VNet 間の名前解決と P2S の関係を確認
  • Always-On VPN プロファイル設定の自動化

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?