🏰

TerraformでAzure Bastionを構築する

2022/05/23に公開

はじめに

みなさんは踏み台サーバを利用されていますか?
踏み台サーバーを使用する理由は、中継サーバを作りセキュリティ強化を行うことなどが挙げられます。Azureには、プロビジョニングされる仮想ネットワーク内のすべてのVMに対して安全な RDPおよびSSH接続を提供する、Bastionという製品があります。
最近Azure Bastinを使う場面が何度かあったので少し書いていきます。

Azure Bastionとは

Azure Bastion は、ブラウザーと Azure portal を使用して仮想マシンに接続できるようにするサービスです。
Bastionを使うことで、TLS 経由で Azure portal から直接、仮想マシンに安全かつシームレスに RDP/SSH 接続できます。 Azure Bastion 経由で接続する場合、仮想マシンにパブリック IP アドレス、エージェント、クライアント または特別なクライアントソフトウェアは必要ありません。

詳細はこちら
https://docs.microsoft.com/ja-jp/azure/bastion/bastion-overview

動作環境

  • macOS Big Sur: 11.5.1
  • azure-cli: 2.35.0
  • Terraform: v1.1.3 on darwin_amd64
  • Provider azurerm: 3.7.0

Teraformによる構築

以下の構成で作っていきます。

Diagramsというツールを使って図を作成してみたのですが、なかなか上手く作れず。。慣れたらいい感じに作れるようになるのだろうか。

Terraform backendは特に指定せず、ローカル環境を使います。

リソースグループ作成

最初にリソースグループを作成します。お好きなリージョンで作成してください。

resource "azurerm_resource_group" "rg_bastion" {
  name     = "rg-bas"
  location = "japaneast"
}

ネットワークリソース作成

Bastionを使用する際はSubnetの名前をAzureBastionSubnetにする必要があります。

resource "azurerm_virtual_network" "vn_bastion" {
  name                = "vn-bas"
  location            = azurerm_resource_group.rg_bastion.location
  resource_group_name = azurerm_resource_group.rg_bastion.name
  address_space       = ["10.3.0.0/16"]
  dns_servers         = ["10.0.0.4", "10.0.0.5"]
}

resource "azurerm_subnet" "sn_bastion" {
  name                 = "AzureBastionSubnet"
  resource_group_name  = azurerm_resource_group.rg_bastion.name
  virtual_network_name = azurerm_virtual_network.vn_bastion.name
  address_prefixes     = ["10.3.1.0/24"]
}

resource "azurerm_subnet" "sn_private" {
  name                 = "sn-private"
  resource_group_name  = azurerm_resource_group.rg_bastion.name
  virtual_network_name = azurerm_virtual_network.vn_bastion.name
  address_prefixes     = ["10.3.2.0/24"]
}

resource "azurerm_subnet_network_security_group_association" "nsg_association_bastion" {
  subnet_id                 = azurerm_subnet.sn_bastion.id
  network_security_group_id = azurerm_network_security_group.nsg_bastion.id
}

resource "azurerm_subnet_network_security_group_association" "nsg_association_private" {
  subnet_id                 = azurerm_subnet.sn_private.id
  network_security_group_id = azurerm_network_security_group.nsg_private.id
}

Bastion作成

BastionのSKUはBasicとStandardが選択できます。
tunneling_enabledなどの設定値はSKUがStandardの場合に使用できます。

違いはこちら
https://docs.microsoft.com/ja-jp/azure/bastion/configuration-settings

resource "azurerm_public_ip" "pip_bastion" {
  name                = "pip-bas"
  location            = azurerm_resource_group.rg_bastion.location
  resource_group_name = azurerm_resource_group.rg_bastion.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_bastion_host" "host_bastion" {
  name                = "host-bas"
  location            = azurerm_resource_group.rg_bastion.location
  resource_group_name = azurerm_resource_group.rg_bastion.name
  sku                 = Standard
  tunneling_enabled   = true

  ip_configuration {
    name                 = "vm_ip_configuration"
    subnet_id            = azurerm_subnet.sn_bastion.id
    public_ip_address_id = azurerm_public_ip.pip_bastion.id
  }
}

NSGの作成

BastionのパブリックIPでは、イグレストラフィック用にポート443が有効になっている必要があります。
その他、細々したルールはこちらにあります。
https://docs.microsoft.com/ja-jp/azure/bastion/bastion-nsg

resource "azurerm_network_security_group" "nsg_bastion" {
  name                = "nsg-bastion"
  location            = azurerm_resource_group.rg_bastion.location
  resource_group_name = azurerm_resource_group.rg_bastion.name

  security_rule {
    name                       = "AllowGatewayInBound"
    priority                   = 1000
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "TCP"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "GatewayManager"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "AllowInternetInBound"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "TCP"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "Internet"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "AllowPrivateVnetOutBound"
    priority                   = 1001
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "TCP"
    source_port_range          = "*"
    destination_port_ranges     = ["3389","22"]
    source_address_prefix      = "*"
    destination_address_prefix = "VirtualNetwork"
  }

 security_rule {
    name                       = "AllowAzureCloudOutBound"
    priority                   = 1002
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "TCP"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "*"
    destination_address_prefix = "AzureCloud"
  }
}

resource "azurerm_network_security_group" "nsg_private" {
  name                = "nsg-private"
  location            = azurerm_resource_group.rg_bastion.location
  resource_group_name = azurerm_resource_group.rg_bastion.name

    security_rule {
    name                       = "AllowBastionVnetInBound"
    priority                   = 1000
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_ranges     = ["3389","22"]
    source_address_prefix      = "VirtualNetwork"
    destination_address_prefix = "*"
  }

    security_rule {
    name                       = "AllowBastionVnetOutBound"
    priority                   = 1000
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_ranges     = ["3389","22"]
    source_address_prefix      = "VirtualNetwork"
    destination_address_prefix = "*"
  }
}

VMの作成

Bastion経由でアクセスするのでパブリックIPは割り当てません。

resource "azurerm_network_interface" "nic_testvm" {
    name                      = "nic-testvm"
    location                  = azurerm_resource_group.rg_bastion.location
    resource_group_name       = azurerm_resource_group.rg_bastion.name

    ip_configuration {
        name                          = "NicConfiguration"
        subnet_id                     = azurerm_subnet.sn_private.id
        private_ip_address_allocation = "Dynamic"
    }
}

resource "azurerm_linux_virtual_machine" "vm_test" {
    name                  = "vm-test"
    location              = azurerm_resource_group.rg_bastion.location
    resource_group_name   = azurerm_resource_group.rg_bastion.name
    network_interface_ids = [azurerm_network_interface.nic_testvm.id]
    size                  = "Standard_B2s"

    os_disk {
        name              = "testVmOsDisk"
        caching           = "ReadWrite"
        storage_account_type = "Standard_LRS"
    }

    source_image_reference {
        publisher = "Canonical"
        offer     = "0001-com-ubuntu-server-focal"
        sku       = "20_04-lts-gen2"
        version   = "latest"
    }

    computer_name  = "mgmt"
    admin_username = "basuser"

    admin_ssh_key {
        username       = "basuser"
        public_key     = tls_private_key.ssh_bastion.public_key_openssh
    }
}

resource "tls_private_key" "ssh_bastion" {
  algorithm = "RSA"
  rsa_bits = 4096
}

resource "local_file" "private_key_pem" {
  filename = "./bas_ssh_key.pem"
  content  = tls_private_key.ssh_bastion.private_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ./bas_ssh_key.pem"
  }
}

Terraform実行

❯❯❯ terraform init   
❯❯❯ terraform plan 
❯❯❯ terraform apply   

リソースの確認

Terraformで作成したリソースがポータル上から確認できました。

さらに、ネイティブクライアントを利用してBastionへのアクセスを参考にiTermからのアクセスを試してみます。
BastionはWeb画面から操作できるので便利ですが、やっぱり普段使い慣れたツールが使える方がいいですよね。
https://docs.microsoft.com/ja-jp/azure/bastion/connect-native-client-windows

set VM_ID (az vm list -g rg-bas --query "[].id" -otsv)

❯❯❯ az network bastion ssh -n host-bas -g rg-bas --target-resource-id $VM_ID --auth-type ssh-key --username basuser --ssh-key ./bas_ssh_key.pem
Command group 'network bastion' is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.13.0-1023-azure x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Mon May 23 14:32:04 UTC 2022

  System load:  0.0               Processes:             114
  Usage of /:   4.9% of 28.90GB   Users logged in:       0
  Memory usage: 6%                IPv4 address for eth0: 10.3.2.4
  Swap usage:   0%


1 update can be applied immediately.
To see these additional updates run: apt list --upgradable


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Sat May 21 04:46:38 2022 from 10.3.1.4
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

basuser@mgmt:~$

Bastionを通して、パブリックIPを持たないVMにアクセスすることができました。
他にもパスワードやAADを利用したアクセスも可能でした。
https://docs.microsoft.com/ja-jp/cli/azure/network/bastion?view=azure-cli-latest#az-network-bastion-ssh

ただ、Azure Key Vaultとの連携間まだできないようでした。(これからできるようになるのかな?)

まとめ

Terraformを使って簡単にBastionを利用した接続感橋を構築することができました。マネージドサービスなので簡単にセキュアな接続環境が構築できる点がとても嬉しいですね。
ただそこそこな金額(最初の 5 GB/月)なので、自分でセキュアな環境を構築できる人はあまり有り難みがないのかもしれませんね。

また今回初めてネイティブクライアントを使用してBastion経由で仮想マシンにアクセスしてみました。今までBastionを使うときはWebブラウザからアクセスしていたので、普段使い慣れたクライアントアプリからアクセスできるのはとても便利だと感じました。

Discussion