Bastion hosts that default to the free Developer SKU (one attribute and you can connect), scaling to Basic, Standard, and Premium when you need them.
Bastion hosts keyed by name. The default SKU is Developer: the lightweight shared-pool offering
that just attaches to a virtual network (set virtual_network_id and you are done), needs no
AzureBastionSubnet, no public IP, no scale units, and costs nothing, which makes the module the
fastest way to get secure VM access going.
Scaling up is per host: sku = "Basic" | "Standard" | "Premium" with an ip_configuration (the
dedicated AzureBastionSubnet at /26 or larger, plus a public_ip_address_id that comes from the
public-ip module: this module never creates public IPs, it only accepts them as inputs). Standard
adds scale_units, zones, and the feature toggles (file copy, IP connect, Kerberos, shareable links,
tunneling); Premium adds session recording and may omit the public IP for a private-only bastion.
Every SKU/feature mismatch fails the plan with a specific message rather than at the API.
module "bastion" {
source = "libre-devops/bastion/azurerm"
version = "~> 4.0"
resource_group_id = module.rg.ids["rg-ldo-uks-prd-001"]
location = "uksouth"
tags = module.tags.tags
bastion_hosts = {
# The Developer default: one attribute.
"bas-ldo-uks-prd-001" = {
virtual_network_id = module.network.vnet_id
}
# Scaled up when needed.
"bas-ldo-hub-uks-prd-001" = {
sku = "Standard"
ip_configuration = {
subnet_id = module.network.subnet_ids["AzureBastionSubnet"]
public_ip_address_id = module.public_ip.public_ip_ids["pip-bas-ldo-uks-prd-001"]
}
tunneling_enabled = true
}
}
}examples/minimal- a Developer bastion attached to a vnet: the one-attribute path.examples/complete- a Developer host alongside a Standard host with scale units, zones, the feature toggles, and the public IP passed in from the public-ip module (one bastion per vnet, so each gets its own). Premium session recording is exercised in the mocked tests; a Premium host adds nothing to the example but cost.
Local work needs PowerShell 7+ and just, because the recipes
wrap the LibreDevOpsHelpers
PowerShell module (the same engine the libre-devops/terraform-azure action runs in CI). Install
just with brew install just, or uv tool add rust-just then uv run just <recipe>.
Run just to list recipes: just update-ldo-pwsh (install or force-update LibreDevOpsHelpers from
PSGallery), just validate, just scan (Trivy only), just pwsh-analyze (PSScriptAnalyzer only),
just plan, just apply, just destroy, just e2e, just test, and just docs (the
plan/apply/destroy recipes mirror the action, including the storage firewall dance; just e2e
applies an example then always destroys it, defaulting to minimal, so nothing is left running).
Releasing is also just:
just increment-release [patch|minor|major] bumps, tags, and publishes a GitHub release, and the
Terraform Registry picks up the tag.
This module is scanned with Trivy; HIGH and CRITICAL
findings fail the build. Any waiver is a deliberate, reviewed decision, never a way to quiet a
finding that should be fixed. Waivers live in .trivyignore.yaml (the
machine-applied source of truth, passed to Trivy with --ignorefile) and are mirrored in a table
here so the reason is auditable.
There are currently no exceptions: the module and its examples scan clean. A bastion exists to REMOVE public management-port exposure, so there is nothing to waive.
To add an exception: add an entry to .trivyignore.yaml (id, optional paths to scope it, and a
statement recording why), then add a matching row here recording the reason. Both the file and
the table are reviewed in the pull request.
The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.
| Name | Version |
|---|---|
| terraform | >= 1.9.0, < 2.0.0 |
| azurerm | >= 4.0.0, < 5.0.0 |
| Name | Version |
|---|---|
| azurerm | >= 4.0.0, < 5.0.0 |
No modules.
| Name | Type |
|---|---|
| azurerm_bastion_host.this | resource |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| bastion_hosts | The bastion hosts to create, keyed by name. The default SKU is DEVELOPER: the lightweight (free) shared-pool offering that just attaches to a virtual network (set virtual_network_id), needs no AzureBastionSubnet, no public IP, and no scale units, so getting going is one attribute. Scale up per host by setting sku: - Basic: needs ip_configuration { subnet_id (an AzureBastionSubnet, /26 or larger), and a public_ip_address_id from the public-ip module (this module never creates public IPs) }. - Standard: Basic plus scale_units (2-50) and the optional features file_copy_enabled, ip_connect_enabled, kerberos_enabled, shareable_link_enabled, tunneling_enabled. - Premium: Standard plus session_recording_enabled, and public_ip_address_id may be omitted for a private-only bastion. copy_paste_enabled works on every SKU (default true). zones is supported on Standard and Premium. Feature/SKU mismatches fail the plan with a specific message rather than at the API. |
map(object({ |
{} |
no |
| location | Azure region for the bastion hosts. | string |
n/a | yes |
| resource_group_id | Resource id of the resource group to create the bastion hosts in. The name is parsed from it (pass the rg module's ids output). | string |
n/a | yes |
| tags | Tags applied to every bastion host (merged with any per-host tags). | map(string) |
{} |
no |
| Name | Description |
|---|---|
| bastion_hosts | The bastion hosts, keyed by name. Full resource objects (all attributes). |
| dns_names | Map of bastion name to the bastion DNS name. |
| ids | Map of bastion name to resource id. |
| ids_zipmap | Map of bastion name to { name, id }, for easy composition with other modules. |
| names | Map of bastion name to name (convenience passthrough). |
| private_only | Map of bastion name to whether the host is private-only (Premium without a public IP). |
| resource_group_name | The resource group the bastion hosts live in, parsed from resource_group_id. |