Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions infrastructure/modules/bastion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# bastion

Deploy an [Azure Bastion host](https://learn.microsoft.com/en-us/azure/bastion/bastion-overview) with a dedicated public IP address and diagnostic settings. Integrates with the [subnet module](../subnet/) and [log-analytics-workspace module](../log-analytics-workspace/).

## Terraform documentation

For the list of inputs, outputs, resources... check the [terraform module documentation](tfdocs.md).

## Prerequisites

Azure Bastion requires a dedicated subnet named exactly `AzureBastionSubnet` with a minimum `/26` prefix. Use the [subnet module](../subnet/) to create it with the required NSG rules:

```hcl
module "subnet_bastion" {
source = "../subnet"

name = "AzureBastionSubnet"
resource_group_name = azurerm_resource_group.main.name
vnet_name = module.vnet.name
address_prefixes = [cidrsubnet(var.vnet_address_space, 10, 0)]
location = var.region
create_nsg = true
network_security_group_name = "nsg-bastion-${var.env_config}-uks"
network_security_group_nsg_rules = local.bastion_nsg_rules

log_analytics_workspace_id = module.log_analytics_workspace.id
monitor_diagnostic_setting_network_security_group_enabled_logs = []
}
```

See [Azure Bastion NSG requirements](https://learn.microsoft.com/en-us/azure/bastion/bastion-nsg) for the required inbound and outbound rules.

## Usage

Deploy a Standard Bastion host with audit logging:

```hcl
module "bastion" {
source = "../../../dtos-devops-templates/infrastructure/modules/bastion"

name = "bas-myapp-dev-uks"
public_ip_name = "pip-bastion-myapp-dev-uks"
resource_group_name = azurerm_resource_group.main.name
location = "uksouth"
sku = "Standard"
subnet_id = module.subnet_bastion.id

log_analytics_workspace_id = module.log_analytics_workspace.id
monitor_diagnostic_setting_bastion_enabled_logs = ["BastionAuditLogs"]
monitor_diagnostic_setting_bastion_metrics = ["AllMetrics"]
}
```

## SKU selection

| SKU | Scale units | Native client | File copy | IP connect | Session recording | Public IP required |
|-----|-------------|---------------|-----------|------------|-------------------|--------------------|
| Basic | Fixed (2) | No | No | No | No | Yes |
| Standard | 2–50 | Yes | Yes | Yes | No | Yes |
| Premium | 2–50 | Yes | Yes | Yes | Yes | Yes |

- **Standard** is recommended for most production workloads. It supports native client connections, host scaling, and all advanced features.
- **Premium** adds session recording for compliance requirements and a private-only deployment option (no public IP).
- **Basic** provides a fixed-capacity dedicated deployment at lower cost. Advanced features are not available.
- **Developer** SKU is not supported by this module — it uses shared infrastructure and does not require a public IP or dedicated subnet.

## Zone redundancy (recommended for production)

By default the public IP is deployed with no availability zone pinning. For production, pass `zones = ["1", "2", "3"]` to make the public IP zone-redundant:

```hcl
module "bastion" {
...
zones = ["1", "2", "3"]
}
```

Zone-redundant public IPs are available in UK South at no additional cost. See [Reliability in Azure Bastion](https://learn.microsoft.com/en-us/azure/reliability/reliability-bastion) for supported regions.

## Standard/Premium features

The following features are disabled by default and require Standard SKU or higher:

**Native client / tunneling** — enables SSH and RDP connections via the Azure CLI (`az network bastion ssh`, `az network bastion rdp`) instead of the browser-based client. Recommended for operations teams:

```hcl
module "bastion" {
...
tunneling_enabled = true
}
```

**IP connect** — allows connecting to VMs using their private IP address rather than requiring them to be in the same virtual network:

```hcl
module "bastion" {
...
ip_connect_enabled = true
}
```

**File copy** — enables file transfer between the local machine and target VMs via native client connections:

```hcl
module "bastion" {
...
file_copy_enabled = true
tunneling_enabled = true # file copy requires native client
}
```

## Scaling

Each scale unit supports approximately 20 concurrent RDP sessions or 40 concurrent SSH sessions. The default of 2 units (40 RDP / 80 SSH) is sufficient for most environments. Increase `scale_units` for larger deployments:

```hcl
module "bastion" {
...
scale_units = 5 # ~100 concurrent RDP / ~200 concurrent SSH
}
```

`scale_units` is only configurable for Standard and Premium SKUs. Basic is fixed at 2 units.

## Diagnostic logs

The module creates a diagnostic setting targeting the provided Log Analytics workspace. The only available log category for Azure Bastion is `BastionAuditLogs`, which records user connections, source IPs, session metadata, and audit trail information:

```hcl
module "bastion" {
...
monitor_diagnostic_setting_bastion_enabled_logs = ["BastionAuditLogs"]
monitor_diagnostic_setting_bastion_metrics = ["AllMetrics"]
}
```
44 changes: 44 additions & 0 deletions infrastructure/modules/bastion/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module "pip" {
source = "../public-ip"

name = var.public_ip_name
resource_group_name = var.resource_group_name
location = var.location
allocation_method = "Static"
sku = "Standard"
zones = var.zones

tags = var.tags
}

resource "azurerm_bastion_host" "bastion" {
name = var.name
location = var.location
resource_group_name = var.resource_group_name
sku = var.sku

copy_paste_enabled = var.copy_paste_enabled
file_copy_enabled = var.file_copy_enabled
ip_connect_enabled = var.ip_connect_enabled
tunneling_enabled = var.tunneling_enabled
shareable_link_enabled = var.shareable_link_enabled
scale_units = var.scale_units

ip_configuration {
name = "configuration"
subnet_id = var.subnet_id
public_ip_address_id = module.pip.id
}

tags = var.tags
}

module "diagnostic_settings" {
source = "../diagnostic-settings"

name = "${var.name}-diagnostic-setting"
target_resource_id = azurerm_bastion_host.bastion.id
log_analytics_workspace_id = var.log_analytics_workspace_id
enabled_log = var.monitor_diagnostic_setting_bastion_enabled_logs
enabled_metric = var.monitor_diagnostic_setting_bastion_metrics
}
19 changes: 19 additions & 0 deletions infrastructure/modules/bastion/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "id" {
description = "The ID of the Bastion host."
value = azurerm_bastion_host.bastion.id
}

output "name" {
description = "The name of the Bastion host."
value = azurerm_bastion_host.bastion.name
}

output "dns_name" {
description = "The FQDN of the Bastion host."
value = azurerm_bastion_host.bastion.dns_name
}

output "public_ip_address" {
description = "The public IP address associated with the Bastion host."
value = module.pip.ip_address
}
170 changes: 170 additions & 0 deletions infrastructure/modules/bastion/tfdocs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Module documentation

## Required Inputs

The following input variables are required:

### <a name="input_log_analytics_workspace_id"></a> [log\_analytics\_workspace\_id](#input\_log\_analytics\_workspace\_id)

Description: The ID of the Log Analytics workspace to send diagnostic logs to.

Type: `string`

### <a name="input_monitor_diagnostic_setting_bastion_enabled_logs"></a> [monitor\_diagnostic\_setting\_bastion\_enabled\_logs](#input\_monitor\_diagnostic\_setting\_bastion\_enabled\_logs)

Description: List of log categories to enable for the Bastion diagnostic setting (e.g. ["BastionAuditLogs"]).

Type: `list(string)`

### <a name="input_monitor_diagnostic_setting_bastion_metrics"></a> [monitor\_diagnostic\_setting\_bastion\_metrics](#input\_monitor\_diagnostic\_setting\_bastion\_metrics)

Description: List of metric categories to enable for the Bastion diagnostic setting (e.g. ["AllMetrics"]).

Type: `list(string)`

### <a name="input_name"></a> [name](#input\_name)

Description: The name of the Azure Bastion host.

Type: `string`

### <a name="input_public_ip_name"></a> [public\_ip\_name](#input\_public\_ip\_name)

Description: The name of the public IP address resource created for the Bastion host.

Type: `string`

### <a name="input_resource_group_name"></a> [resource\_group\_name](#input\_resource\_group\_name)

Description: The name of the resource group in which to create the Bastion host.

Type: `string`

### <a name="input_subnet_id"></a> [subnet\_id](#input\_subnet\_id)

Description: The ID of the AzureBastionSubnet in which the Bastion host will be deployed.

Type: `string`

## Optional Inputs

The following input variables are optional (have default values):

### <a name="input_copy_paste_enabled"></a> [copy\_paste\_enabled](#input\_copy\_paste\_enabled)

Description: Is copy/paste feature enabled for the Bastion host.

Type: `bool`

Default: `true`

### <a name="input_file_copy_enabled"></a> [file\_copy\_enabled](#input\_file\_copy\_enabled)

Description: Is file copy feature enabled for the Bastion host. Requires Standard SKU or higher.

Type: `bool`

Default: `false`

### <a name="input_ip_connect_enabled"></a> [ip\_connect\_enabled](#input\_ip\_connect\_enabled)

Description: Is IP connect feature enabled for the Bastion host. Requires Standard SKU or higher.

Type: `bool`

Default: `false`

### <a name="input_location"></a> [location](#input\_location)

Description: The location/region where the Bastion host will be created.

Type: `string`

Default: `"uksouth"`

### <a name="input_scale_units"></a> [scale\_units](#input\_scale\_units)

Description: The number of scale units for the Bastion host. Each unit supports ~20 concurrent RDP / ~40 concurrent SSH sessions. Must be between 2 and 50 for Standard/Premium SKU; Basic is fixed at 2.

Type: `number`

Default: `2`

### <a name="input_shareable_link_enabled"></a> [shareable\_link\_enabled](#input\_shareable\_link\_enabled)

Description: Is shareable link feature enabled for the Bastion host. Requires Standard SKU or higher.

Type: `bool`

Default: `false`

### <a name="input_sku"></a> [sku](#input\_sku)

Description: The SKU tier of the Bastion host. Possible values are Basic, Standard, and Premium. Standard is recommended for most production workloads; Premium adds session recording and private-only deployment. Developer SKU is not supported by this module as it does not use a public IP or dedicated subnet.

Type: `string`

Default: `"Standard"`

### <a name="input_tags"></a> [tags](#input\_tags)

Description: A mapping of tags to assign to the resource.

Type: `map(string)`

Default: `{}`

### <a name="input_tunneling_enabled"></a> [tunneling\_enabled](#input\_tunneling\_enabled)

Description: Is tunneling (native client support) feature enabled for the Bastion host. Enables native SSH/RDP client connections via az network bastion ssh/rdp. Recommended for Standard SKU or higher.

Type: `bool`

Default: `false`

### <a name="input_zones"></a> [zones](#input\_zones)

Description: Availability zones for the public IP address. Use ["1", "2", "3"] for zone-redundant deployment, which is recommended for production environments. An empty list deploys with no zone redundancy.

Type: `list(string)`

Default: `[]`
## Modules

The following Modules are called:

### <a name="module_diagnostic_settings"></a> [diagnostic\_settings](#module\_diagnostic\_settings)

Source: ../diagnostic-settings

Version:

### <a name="module_pip"></a> [pip](#module\_pip)

Source: ../public-ip

Version:
## Outputs

The following outputs are exported:

### <a name="output_dns_name"></a> [dns\_name](#output\_dns\_name)

Description: The FQDN of the Bastion host.

### <a name="output_id"></a> [id](#output\_id)

Description: The ID of the Bastion host.

### <a name="output_name"></a> [name](#output\_name)

Description: The name of the Bastion host.

### <a name="output_public_ip_address"></a> [public\_ip\_address](#output\_public\_ip\_address)

Description: The public IP address associated with the Bastion host.
## Resources

The following resources are used by this module:

- [azurerm_bastion_host.bastion](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/bastion_host) (resource)
Loading
Loading