Azure Bastion na Landing Zone sem IP Público

Azure Bastion na Landing Zone sem IP Público

Azure Bastion na Landing Zone sem IP Público

Azure Bastion na Landing Zone sem IP Público

Neste artigo você vai configurar o Azure Bastion sem IP público na Landing Zone Hub-Spoke, permitindo acesso RDP e SSH às VMs sem expor nenhum endereço público nas máquinas virtuais. O Azure Bastion sem IP público nas VMs é o modelo recomendado para ambientes corporativos: o Bastion atua como jump host gerenciado diretamente no Hub, eliminando a superfície de ataque de IPs expostos. Vamos implantar dois Bastions — um em Brazil South e outro em East US — com SKU Standard, tunneling nativo e IP connect habilitados, usando Terraform e a AzureBastionSubnet já criada no artigo anterior.

Série: DR Azure e Landing Zone
  • 📖 Art. 01: Planejamento de DR Azure e Landing Zone
  • 📖 Art. 02: Hub-Spoke Landing Zone com Terraform
  • 📖 Art. 03: Azure Firewall e NSGs na Landing Zone
  • ⚙️ Art. 04 (este): Azure Bastion na Landing Zone sem IP Público
  • 🔒 Art. 05: Azure Front Door e Traffic Manager para Failover
  • 🔒 Art. 06: DNS Privado Multi-Região no Azure
  • 🔒 Art. 07: Azure Site Recovery para VMs Multi-Região
  • 🔒 Art. 08: Azure Storage com Geo-Replicação para DR
  • 🔒 Art. 09: AKS Multi-Região com Failover no Azure
  • 🔒 Art. 10: Velero no AKS para Backup e Restore Cross-Region
  • 🔒 Art. 11: Runbooks de Failover no Azure Automation
  • 🔒 Art. 12: Simular um DR no Azure sem Impacto em Produção
  • 🔒 Art. 13: Monitoramento do DR no Azure com Azure Monitor

Sumário

O que é Azure Bastion e por que usar sem IP público

O Azure Bastion sem IP público nas VMs é um serviço PaaS gerenciado pela Microsoft que fornece conectividade RDP e SSH segura para máquinas virtuais diretamente pelo portal do Azure ou por clientes nativos. Em vez de abrir portas RDP/SSH na internet, todo o acesso passa pelo Bastion — que é o único recurso exposto publicamente, com IP público próprio e protegido por TLS.

O conceito central do azure bastion sem ip publico nas VMs é simples: as máquinas virtuais do Spoke ficam sem nenhum IP público. Isso significa que não há como acessá-las diretamente pela internet, mesmo que o NSG permita — simplesmente não existe endereço de destino. O acesso sempre passa pelo Bastion no Hub, que usa peering para alcançar as VMs nos Spokes.

Modelo de acesso Com IP público na VM Com Azure Bastion sem IP público
Superfície de ataque IP da VM exposto na internet Apenas o Bastion exposto
Porta RDP/SSH aberta 3389/22 na internet Porta 443 via TLS no Bastion
Auditoria de acesso Dependente de logs de VM Logs centralizados no Azure Monitor
Custo adicional IP público por VM (~R$ 20/mês cada) Uma instância Bastion por Hub
Compatibilidade Qualquer cliente RDP/SSH Portal, cliente nativo (SKU Standard)

SKU Basic vs Standard — qual escolher

O Azure Bastion tem dois SKUs principais. Para um ambiente de Landing Zone corporativo com DR multi-região, o SKU Standard é obrigatório pois habilita tunneling nativo e IP connect — recursos essenciais para integrar ferramentas como VS Code Remote SSH e Windows Terminal.

Recurso Basic Standard
Acesso via portal
Clipboard
Tunneling nativo (SSH/RDP via CLI)
IP Connect (IP privado direto)
Sessões simultâneas (scale units) Fixo (2) 2–50 (escalável)
Shareable Link
Custo (por hora, EastUS 2024) ~$0.19 ~$0.29 + scale units

Neste lab usamos SKU Standard com 2 scale units — suficiente para testes. Em produção, ao operar Azure Bastion sem IP público em múltiplas regiões, aumente os scale units conforme o número de sessões simultâneas esperadas.

Arquitetura do lab

Cada Hub VNet — vnet-hub-blog-castilho-brazilsouth e vnet-hub-blog-castilho-eastus — já possui uma AzureBastionSubnet criada no Art. 02, com prefixo /26. O Azure Bastion sem IP público nas VMs precisa obrigatoriamente desta subnet com esse nome exato. Vamos implantar um Bastion em cada Hub, criando apenas o IP público e o host Bastion.

Recurso Região Subnet
pip-bastion-hub-blog-castilho-brazilsouth Brazil South — (IP público do Bastion)
bastion-hub-blog-castilho-brazilsouth Brazil South AzureBastionSubnet (10.0.0.128/26)
pip-bastion-hub-blog-castilho-eastus East US — (IP público do Bastion)
bastion-hub-blog-castilho-eastus East US AzureBastionSubnet (10.1.0.128/26)

O fluxo de acesso com Azure Bastion sem IP público nas VMs é: operador → portal Azure (HTTPS 443) → Bastion no Hub → peering → VM no Spoke. A VM nunca é atingida diretamente pela internet.

Pré-requisitos

  • Hub-Spoke Landing Zone implantada (Art. 02) — VNets e AzureBastionSubnet existentes
  • Terraform ≥ 1.5 instalado
  • Azure CLI autenticado: az login
  • Permissão Contributor nos Resource Groups de rede (rg-blog-castilho-network-*)
  • Storage Account de remote state configurado (bootstrap do Art. 02)
Segurança: Os comandos deste artigo utilizam variáveis de ambiente para credenciais. Nunca insira Subscription IDs ou senhas diretamente nos comandos. Use um arquivo .env local (não versionado) para armazenar esses valores.

Variáveis de ambiente

Defina as variáveis antes de executar qualquer comando Terraform ou Azure CLI:

# Identidade Azure
export AZURE_SUBSCRIPTION_ID="<SUBSCRIPTION_ID>"

# Remote state (criado no bootstrap do Art. 02)
export TF_STATE_RG="rg-blog-castilho-tfstate"
export TF_STATE_SA="<storage-account-name>"
export TF_STATE_CONTAINER="tfstate"

Passo 1 — Código Terraform

Cada Bastion tem sua própria pasta de Terraform com três arquivos: providers.tf, variables.tf e main.tf. Veja a estrutura do Bastion de Brazil South:

providers.tf

terraform {
  required_version = ">= 1.5"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
  backend "azurerm" {}
}

provider "azurerm" {
  subscription_id = var.subscription_id
  features {}
}

O bloco backend "azurerm" {} vazio é intencional — os parâmetros de conexão com o Storage Account são passados via backend.hcl no momento do terraform init, mantendo o código sem credenciais hardcoded.

variables.tf

variable "subscription_id" {
  type      = string
  sensitive = true
}

variable "sku" {
  type    = string
  default = "Standard"
}

variable "scale_units" {
  type    = number
  default = 2
}

variable "tags" {
  type = map(string)
  default = {
    project    = "blog-castilho"
    managed_by = "terraform"
    artigo     = "art-04-bastion"
  }
}

main.tf — Brazil South

O main.tf usa data sources para referenciar o Resource Group e a subnet criados no Art. 02, sem precisar de remote state:

data "azurerm_resource_group" "this" {
  name = "rg-blog-castilho-network-brazilsouth"
}

data "azurerm_subnet" "bastion" {
  name                 = "AzureBastionSubnet"
  virtual_network_name = "vnet-hub-blog-castilho-brazilsouth"
  resource_group_name  = data.azurerm_resource_group.this.name
}

resource "azurerm_public_ip" "this" {
  name                = "pip-bastion-hub-blog-castilho-brazilsouth"
  resource_group_name = data.azurerm_resource_group.this.name
  location            = data.azurerm_resource_group.this.location
  allocation_method   = "Static"
  sku                 = "Standard"
  tags                = var.tags
}

resource "azurerm_bastion_host" "this" {
  name                = "bastion-hub-blog-castilho-brazilsouth"
  resource_group_name = data.azurerm_resource_group.this.name
  location            = data.azurerm_resource_group.this.location
  sku                 = var.sku
  scale_units         = var.scale_units
  tunneling_enabled   = true
  ip_connect_enabled  = true
  tags                = var.tags

  ip_configuration {
    name                 = "ipconfig1"
    subnet_id            = data.azurerm_subnet.bastion.id
    public_ip_address_id = azurerm_public_ip.this.id
  }
}

Os atributos tunneling_enabled e ip_connect_enabled requerem SKU Standard. O ip_connect_enabled permite acessar qualquer VM pelo endereço IP privado, mesmo que ela não esteja visível no portal — útil para VMs recém-criadas ou em Spokes sem peering direto com o Hub do Bastion.

main.tf — East US

O Bastion de East US segue a mesma estrutura, apenas trocando os nomes dos recursos e referenciando a VNet correta:

data "azurerm_resource_group" "this" {
  name = "rg-blog-castilho-network-eastus"
}

data "azurerm_subnet" "bastion" {
  name                 = "AzureBastionSubnet"
  virtual_network_name = "vnet-hub-blog-castilho-eastus"
  resource_group_name  = data.azurerm_resource_group.this.name
}

resource "azurerm_public_ip" "this" {
  name                = "pip-bastion-hub-blog-castilho-eastus"
  resource_group_name = data.azurerm_resource_group.this.name
  location            = data.azurerm_resource_group.this.location
  allocation_method   = "Static"
  sku                 = "Standard"
  tags                = var.tags
}

resource "azurerm_bastion_host" "this" {
  name                = "bastion-hub-blog-castilho-eastus"
  resource_group_name = data.azurerm_resource_group.this.name
  location            = data.azurerm_resource_group.this.location
  sku                 = var.sku
  scale_units         = var.scale_units
  tunneling_enabled   = true
  ip_connect_enabled  = true
  tags                = var.tags

  ip_configuration {
    name                 = "ipconfig1"
    subnet_id            = data.azurerm_subnet.bastion.id
    public_ip_address_id = azurerm_public_ip.this.id
  }
}

Passo 2 — Executar o Terraform

Cada pasta de recurso tem seu próprio estado Terraform no remote state. O fluxo é: gerar o backend.hcl, inicializar e aplicar:

# Brazil South
cd scripts/art-04-bastion/bastion-hub-blog-castilho-brazilsouth

cat > backend.hcl <<EOF
resource_group_name  = "$TF_STATE_RG"
storage_account_name = "$TF_STATE_SA"
container_name       = "$TF_STATE_CONTAINER"
key                  = "bastion-hub-blog-castilho-brazilsouth.tfstate"
EOF

terraform init -backend-config=backend.hcl -reconfigure
terraform apply -var subscription_id="$AZURE_SUBSCRIPTION_ID"

Repita para East US alterando o diretório e a chave do state. O provisionamento leva entre 5 e 10 minutos — o Bastion é um serviço que exige inicialização da instância gerenciada pela Microsoft.

# East US
cd scripts/art-04-bastion/bastion-hub-blog-castilho-eastus

cat > backend.hcl <<EOF
resource_group_name  = "$TF_STATE_RG"
storage_account_name = "$TF_STATE_SA"
container_name       = "$TF_STATE_CONTAINER"
key                  = "bastion-hub-blog-castilho-eastus.tfstate"
EOF

terraform init -backend-config=backend.hcl -reconfigure
terraform apply -var subscription_id="$AZURE_SUBSCRIPTION_ID"

Ao concluir, o Terraform mostra as informações dos dois recursos criados:

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

# Recursos criados:
# azurerm_public_ip.this       → pip-bastion-hub-blog-castilho-brazilsouth
# azurerm_bastion_host.this    → bastion-hub-blog-castilho-brazilsouth

Passo 3 — Configurar RBAC

O Azure Bastion sem IP público depende de RBAC bem configurado para controlar quem pode abrir sessões. São necessárias permissões em dois escopos: no Bastion (para usar o serviço) e na VM (para autenticar na máquina).

Role Escopo Permite
Reader Bastion Host Visualizar e usar o Bastion no portal
Reader VM ou Resource Group Ver a VM na lista do portal
Virtual Machine Administrator Login VM Login com privilégios de administrador
Virtual Machine User Login VM Login como usuário padrão

Atribua as roles necessárias com Azure CLI. Substitua <OBJECT_ID> pelo Object ID do usuário ou grupo no Azure AD:

SUBSCRIPTION_ID="<SUBSCRIPTION_ID>"
OBJECT_ID="<OBJECT_ID>"
VM_RG="rg-blog-castilho-workload-brazilsouth"
VM_NAME="vm-drlab-brazilsouth"   # nome da VM de teste
BASTION_RG="rg-blog-castilho-network-brazilsouth"
BASTION_NAME="bastion-hub-blog-castilho-brazilsouth"

# Reader no Bastion
az role assignment create \
  --assignee "$OBJECT_ID" \
  --role "Reader" \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$BASTION_RG/providers/Microsoft.Network/bastionHosts/$BASTION_NAME"

# Virtual Machine Administrator Login na VM
az role assignment create \
  --assignee "$OBJECT_ID" \
  --role "Virtual Machine Administrator Login" \
  --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$VM_RG/providers/Microsoft.Compute/virtualMachines/$VM_NAME"

Passo 4 — Acessar VMs pelo portal

Com o Bastion provisionado e o RBAC configurado, o acesso pelo portal do Azure funciona sem instalar nada na máquina cliente:

  1. No portal, navegue até a VM desejada no Spoke
  2. No menu lateral, clique em Connect → Bastion
  3. O Azure detecta automaticamente o Bastion disponível no Hub via peering
  4. Informe usuário e senha (ou chave SSH para VMs Linux)
  5. A sessão RDP ou SSH abre diretamente no browser via WebSocket sobre TLS 443

Nenhuma porta RDP (3389) ou SSH (22) precisa estar aberta nos NSGs para o tráfego externo. O Bastion se comunica internamente com a VM pela VNet, então apenas o tráfego de rede interna é necessário.

Passo 5 — Acesso via cliente nativo (SSH/RDP)

O SKU Standard com tunneling_enabled = true permite usar clientes nativos como ssh, Windows Terminal ou VS Code Remote SSH. Isso é feito criando um túnel local pelo Azure CLI:

# Criar túnel local para SSH (porta 2222 local → VM)
az network bastion tunnel \
  --name bastion-hub-blog-castilho-brazilsouth \
  --resource-group rg-blog-castilho-network-brazilsouth \
  --target-resource-id "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/rg-blog-castilho-workload-brazilsouth/providers/Microsoft.Compute/virtualMachines/vm-drlab-brazilsouth" \
  --resource-port 22 \
  --port 2222

Com o túnel ativo, conecte pelo cliente SSH em outro terminal:

ssh azureuser@127.0.0.1 -p 2222

Para RDP, o processo é o mesmo: substitua --resource-port 22 por 3389 e --port 2222 por qualquer porta local livre, depois conecte com o cliente RDP apontando para 127.0.0.1:<porta>.

JIT Access com Azure Bastion

O Just-In-Time (JIT) Access é uma funcionalidade do Microsoft Defender for Cloud que complementa o Azure Bastion. Com JIT, as portas de gerenciamento (3389, 22) ficam bloqueadas por padrão nos NSGs, e são abertas temporariamente apenas quando um usuário solicita acesso — com aprovação, horário e IP de origem controlados.

Combinando JIT com Azure Bastion sem IP público, o acesso às VMs exige: autenticação no Azure AD + permissão JIT aprovada + sessão pelo Bastion. São três camadas de controle sem nenhum IP público exposto nas VMs.

Para habilitar JIT no Microsoft Defender for Cloud:

# Habilitar Defender for Cloud no Resource Group de workload
az security pricing create \
  --name VirtualMachines \
  --tier "Standard"

# Habilitar JIT em uma VM específica
az security jit-policy create \
  --resource-group rg-blog-castilho-workload-brazilsouth \
  --name default \
  --virtual-machines '[
    {
      "id": "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/rg-blog-castilho-workload-brazilsouth/providers/Microsoft.Compute/virtualMachines/vm-drlab-brazilsouth",
      "ports": [
        {"number": 22, "protocol": "TCP", "allowedSourceAddressPrefix": "*", "maxRequestAccessDuration": "PT3H"},
        {"number": 3389, "protocol": "TCP", "allowedSourceAddressPrefix": "*", "maxRequestAccessDuration": "PT3H"}
      ]
    }
  ]'

Verificar a configuração

Após o deploy, confirme que os recursos de Azure Bastion sem IP público foram criados corretamente nas duas regiões:

# Listar Bastions no Resource Group
az network bastion list \
  --resource-group rg-blog-castilho-network-brazilsouth \
  --output table

# Exemplo de output esperado:
# Name                                    Location      ProvisioningState    Sku
# --------------------------------------  ------------  -------------------  --------
# bastion-hub-blog-castilho-brazilsouth   brazilsouth   Succeeded            Standard

# Verificar IP público associado
az network public-ip show \
  --resource-group rg-blog-castilho-network-brazilsouth \
  --name pip-bastion-hub-blog-castilho-brazilsouth \
  --query "{name:name, ip:ipAddress, allocationMethod:publicIPAllocationMethod}" \
  --output table

# Exemplo de output esperado:
# Name                                          Ip             AllocationMethod
# --------------------------------------------  -------------  ----------------
# pip-bastion-hub-blog-castilho-brazilsouth     <IP_PÚBLICO>   Static

# Confirmar que nenhuma VM do Spoke tem IP público
az network public-ip list \
  --resource-group rg-blog-castilho-workload-brazilsouth \
  --output table
# Output deve estar vazio — nenhum IP público nas VMs
# Verificar East US
az network bastion list \
  --resource-group rg-blog-castilho-network-eastus \
  --output table

# Exemplo de output esperado:
# Name                               Location    ProvisioningState    Sku
# ---------------------------------  ----------  -------------------  --------
# bastion-hub-blog-castilho-eastus   eastus      Succeeded            Standard

Troubleshooting

Erro Causa Solução
BastionShareableLinkNotEnabled Feature não habilitada no SKU Use SKU Standard e shareable_link_enabled = true
SubnetSizeNotValid AzureBastionSubnet menor que /26 Subnet precisa ser /26 ou maior — recrie a subnet no Art. 02
Botão “Connect via Bastion” não aparece na VM Bastion em VNet diferente sem peering Confirme que o peering Hub-Spoke está ativo e allow_forwarded_traffic = true
ProvisioningState: Failed Timeout ou conflito de recurso Execute terraform apply novamente — o Bastion leva até 10 min para provisionar
Autenticação falha no Bastion Falta de permissão RBAC na VM Atribua Virtual Machine Administrator Login ao usuário no escopo da VM
Túnel nativo fecha imediatamente SKU Basic ou tunneling não habilitado Confirme tunneling_enabled = true e SKU Standard no Terraform

Limpeza dos recursos

Para destruir os Bastions e IPs públicos sem afetar o restante da Landing Zone, execute terraform destroy em cada pasta:

cd scripts/art-04-bastion/bastion-hub-blog-castilho-brazilsouth
terraform destroy -var subscription_id="$AZURE_SUBSCRIPTION_ID"

cd scripts/art-04-bastion/bastion-hub-blog-castilho-eastus
terraform destroy -var subscription_id="$AZURE_SUBSCRIPTION_ID"

Isso remove apenas o Bastion e o IP público. A AzureBastionSubnet e a VNet Hub continuam existindo, pois pertencem ao estado Terraform do Art. 02.

Próximos passos

Com o Azure Bastion sem IP público configurado nos dois Hubs, o acesso seguro às VMs está resolvido. O próximo artigo aborda o Azure Front Door e Traffic Manager para controlar o tráfego de entrada externo e orquestrar o failover entre regiões — completando a camada de entrada da Landing Zone.

Série: DR Azure e Landing Zone
  • 📖 Art. 01: Planejamento de DR Azure e Landing Zone
  • 📖 Art. 02: Hub-Spoke Landing Zone com Terraform
  • 📖 Art. 03: Azure Firewall e NSGs na Landing Zone
  • ⚙️ Art. 04 (este): Azure Bastion na Landing Zone sem IP Público
  • 🔒 Art. 05: Azure Front Door e Traffic Manager para Failover
  • 🔒 Art. 06: DNS Privado Multi-Região no Azure
  • 🔒 Art. 07: Azure Site Recovery para VMs Multi-Região
  • 🔒 Art. 08: Azure Storage com Geo-Replicação para DR
  • 🔒 Art. 09: AKS Multi-Região com Failover no Azure
  • 🔒 Art. 10: Velero no AKS para Backup e Restore Cross-Region
  • 🔒 Art. 11: Runbooks de Failover no Azure Automation
  • 🔒 Art. 12: Simular um DR no Azure sem Impacto em Produção
  • 🔒 Art. 13: Monitoramento do DR no Azure com Azure Monitor

Interessado em saber mais sobre artigos relacionados ao Microsoft Azure CLIQUE AQUI

🚀 Vamos nos conectar?

Não perca nenhuma oportunidade! Cadastre-se nas minhas redes e no canal do YouTube para receber conteúdos de TI, Cloud, Azure, Kubernetes e DevOps em primeira mão.

Dica: No Facebook, todos os artigos do blog são publicados automaticamente. Vale a pena curtir!


💬 Dúvidas ou Problemas?

Com o intuito de ajudar a comunidade, caso você tenha dúvidas ou encontre problemas na execução dos comandos deste artigo, deixe um comentário abaixo. Responderei o mais breve possível!

Muito obrigado pela visita e até o próximo post!

Jefferson Castilho Especialista em Cloud & DevOps.

Este guia técnico é exclusivo do Blog do Castilho. Explore nossa para mais conteúdos sobre IA e Cloud.

 

Deixe uma resposta

Rolar para cima

Descubra mais sobre Blog do Castilho - Tecnologia | FinOps | DevOps | Cloud

Assine agora mesmo para continuar lendo e ter acesso ao arquivo completo.

Continue reading