Hardened Linux VMs by default: SSH-only, Trusted Launch, managed identity, a verified image catalog, and VM Insights wired in one attribute.
VMs keyed by name, each with its own NIC, built so the SECURE shape is the ZERO-CONFIG shape:
- SSH-only authentication (passwords are an explicit, plan-visible opt-in).
- Trusted Launch (secure boot + vTPM) on by default.
- A system-assigned managed identity on every VM (what the monitor agent and RBAC grants use).
- Managed boot diagnostics and platform patch assessment on by default.
- Public IPs are inputs only (they live in the public-ip module), and attaching one is plan-visible; the intended door is the bastion module's free Developer SKU.
The image catalog replaces the old external "SKU calculator" modules: source_image_simple
takes a friendly key (Ubuntu2204, Ubuntu2404, Debian12, RHEL9, Sles15, Rocky9; discover
them via the image_catalog_keys output) resolving to marketplace references verified against the
live platform, all Gen2 and Trusted Launch capable; Rocky carries its marketplace plan automatically. source_image_reference and source_image_id remain
first-class, and marketplace plan images get their azurerm_marketplace_agreement created
(deduplicated) when accept_marketplace_agreement = true.
VM Insights in one attribute: vm_insights = { log_analytics_workspace_id = ... } installs the
Azure Monitor agent on every VM (per-VM opt-out), creates the VM Insights data collection rule (or
associates an existing data_collection_rule_id), and associates each VM, using the VMs' managed
identities.
The rest of the surface: flexible identity (SystemAssigned by default, UserAssigned, both, or
None), a cloud_init convenience that base64-encodes plain cloud-init YAML for you (the
often-forgotten step), data disks with auto-assigned LUNs, spot pricing, zones and every placement
option (availability sets, VMSS attachment, proximity groups, capacity reservations, dedicated
hosts), ASG associations, static private IPs, accelerated networking, encryption at host, gallery
applications, key vault certificate secrets, termination and OS image notifications, modern run
commands, and full patching controls. The often-forgotten subscription plumbing is opt-in and
conditional too: resource_provider_feature_registrations (for example
Microsoft.Compute/EncryptionAtHost, which a check reminds you about) and
resource_provider_registrations, both documented as subscription-wide and single-owner.
- Terraform removes the OS disk with the VM by default (configurable via the provider
featuresblock). - All arguments, including the administrator login and password, are stored in the raw state as plain text: protect the state.
azurerm_linux_virtual_machinedoes not support unmanaged disks or attaching existing OS disks (capture an image, or fall back toazurerm_virtual_machine).public_ip_addressoutputs may be unpopulated for Dynamic public IPs.vm_agent_platform_updates_enabledis platform-controlled and read-only; this module does not touch it.
module "linux_vm" {
source = "libre-devops/linux-vm/azurerm"
version = "~> 4.0"
resource_group_id = module.rg.ids["rg-ldo-uks-prd-001"]
location = "uksouth"
tags = module.tags.tags
vm_insights = {
log_analytics_workspace_id = module.log_analytics.workspace_ids["log-ldo-uks-prd-001"]
}
linux_virtual_machines = {
"vm-ldo-app-uks-prd-001" = {
size = "Standard_D2s_v5"
admin_username = "azureuser"
source_image_simple = "Ubuntu2404"
subnet_id = module.network.subnet_ids["snet-app"]
admin_ssh_keys = [{
public_key = module.ssh_key.public_keys_openssh["ssh-ldo-uks-prd-001"]
}]
data_disks = {
"datadisk01-vm-ldo-app-uks-prd-001" = { disk_size_gb = 128 }
}
}
}
}examples/minimal- one VM with the secure defaults from a catalog image and a bring-your-own key.examples/complete- the "secure VM estate in a pinch" build: tags, rg, vnet, forward and reverse private DNS with auto-registration, a free Developer bastion as the door, a firewalled vault holding ephemerally generated SSH keys (write-only, never in state), Log Analytics with VM Insights on every VM, and two hardened VMs exercising the full surface (catalog and explicit images, data disks, spot, zones, static IPs, accelerated networking, run commands).
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.
| ID | Scope | Reason |
|---|---|---|
| AVD-AZU-0013 (vault network ACL default action) | examples/complete/main.tf |
The disposable example vault opts out of the keyvault module's deny-by-default firewall so the CI self-test runner can reach the data plane; real deployments keep the secure default, and the firewalled shape plus the terraform-azure action's key vault dance are documented alongside. |
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 | Description | Type | Default | Required |
|---|---|---|---|---|
| linux_virtual_machines | The Linux VMs to create, keyed by VM name. Each VM gets its own NIC (subnet_id is where it lives; public_ip_address_id is ONLY an input, public IPs live in the public-ip module). SECURE DEFAULTS: SSH-only authentication (disable_password_authentication = true, so admin_ssh_keys is required unless you explicitly enable passwords), Trusted Launch (secure_boot_enabled and vtpm_enabled true; the image catalog entries are all Gen2 and Trusted Launch capable), a system-assigned managed identity, managed boot diagnostics, and platform patch assessment. IMAGE SELECTION, exactly one of: - source_image_simple: a friendly catalog key (Ubuntu2204, Ubuntu2404, Debian12, RHEL9, Sles15, Rocky9; see the image_catalog_keys output), verified Gen2/Trusted Launch marketplace references. Rocky carries its marketplace plan automatically (accept with accept_marketplace_agreement). - source_image_reference: { publisher, offer, sku, version (default latest) } for anything else. - source_image_id: a custom or gallery image id. Marketplace plan images: set plan { name, product, publisher } and optionally accept_marketplace_agreement = true to create the azurerm_marketplace_agreement. NETWORKING per VM: subnet_id (required), private_ip_address (static when set), public_ip_address_id, accelerated_networking_enabled (default false; not every size supports it), ip_forwarding_enabled, dns_servers, application_security_group_ids (associations only; ASGs live with the network modules), nic_name / ipconfig_name overrides. DISKS: os_disk (caching ReadWrite, StandardSSD_LRS by default, plus size, encryption set, security encryption, write accelerator, diff_disk_settings) and data_disks keyed by name (size_gb required; lun auto-assigned by declaration order unless set; storage_account_type, caching, create_option, encryption set, zone follows the VM). EVERYTHING ELSE: zone, availability_set_id, virtual_machine_scale_set_id, proximity_placement_group_id, capacity_reservation_group_id, dedicated_host_id / dedicated_host_group_id, platform_fault_domain, edge_zone; spot { max_bid_price, eviction_policy }; additional_capabilities (ultra SSD, hibernation); encryption_at_host_enabled (subscription feature-gated; see resource_provider_feature_registrations for the often-forgotten registration); identity (SystemAssigned by default, UserAssigned, both as "SystemAssigned, UserAssigned", or None for no identity at all); patching (patch_mode default ImageDefault, patch_assessment_mode default AutomaticByPlatform, reboot_setting, bypass flag); license_type; user_data / custom_data; computer_name (defaults from the VM name); disk_controller_type; extensions_time_budget; gallery_applications; secrets (key vault certificates); termination_notification; os_image_notification; boot_diagnostics_storage_account_uri (unset = managed storage); run_command { script | script_uri | command_id, run_as_user, run_as_password }; cloud_init (plain cloud-init YAML, base64-encoded for you; mutually exclusive with custom_data, which passes through pre-encoded); monitor_agent_enabled (default true, only relevant when vm_insights is set) and per-VM tags. PROVIDER DISCLAIMERS worth knowing: the OS disk is deleted with the VM by default (configurable via the provider features block); all arguments including admin credentials are stored in the raw state as plain text (protect the state); unmanaged disks and attaching existing OS disks are not supported by azurerm_linux_virtual_machine; and public_ip_address outputs may be unpopulated for Dynamic public IPs. |
map(object({ |
{} |
no |
| location | Azure region for the VMs. | string |
n/a | yes |
| resource_group_id | Resource id of the resource group to create the VMs in. The name is parsed from it (pass the rg module's ids output). | string |
n/a | yes |
| resource_provider_feature_registrations | Preview/opt-in features to register, as "Namespace/FeatureName" strings (for example "Microsoft.Compute/EncryptionAtHost", the classic forgotten prerequisite for encryption_at_host_enabled). Empty (the default) manages nothing. Registration is subscription-wide and can take several minutes to propagate. |
set(string) |
[] |
no |
| resource_provider_registrations | Resource provider namespaces to register on the subscription (for example Microsoft.Monitor), for subscriptions where the provider has never been used. Empty (the default) manages nothing. NOTE: a namespace already registered elsewhere will conflict on import/destroy; this is for the deliberate owner only, and the provider's own resource_provider_registrations setting must not also manage it. |
set(string) |
[] |
no |
| tags | Tags applied to every resource this module creates (merged with any per-VM tags). | map(string) |
{} |
no |
| vm_insights | Opt-in VM Insights for every VM in this call: the module installs the Azure Monitor agent (each VM's system-assigned identity is what the agent authenticates with, which the module enables by default), creates the VM Insights data collection rule pointed at log_analytics_workspace_id (or associates an existing one passed as data_collection_rule_id), and associates every VM with it. null (the default) creates nothing. |
object({ |
null |
no |
| Name | Description |
|---|---|
| data_collection_rule_id | The VM Insights data collection rule in effect (created or passed in); null when vm_insights is off. |
| data_disk_ids | Map of "vm|disk" to managed disk id. |
| data_disks | The data disks, keyed "vm|disk". Full resource objects. |
| identity_principal_ids | Map of VM name to the system-assigned identity principal id (what RBAC assignments target; null when the VM has no system identity). |
| ids | Map of VM name to resource id. |
| ids_zipmap | Map of VM name to { name, id }, for easy composition with other modules. |
| image_catalog | The full image catalog (key => { publisher, offer, sku }), verified Gen2 / Trusted Launch capable marketplace references. |
| image_catalog_keys | Every friendly key the image catalog offers for source_image_simple. |
| linux_virtual_machines | The VMs, keyed by name: every attribute except the provider's deprecated vm_agent_platform_updates_enabled (a full-object output would trip its deprecation warning). Sensitive because admin_password and custom_data are inside. |
| names | Map of VM name to name (convenience passthrough). |
| network_interface_ids | Map of VM name to NIC id. |
| network_interfaces | The NICs, keyed by VM name. Full resource objects. |
| private_ip_addresses | Map of VM name to primary private IP address. |
| resource_group_name | The resource group the VMs live in, parsed from resource_group_id. |
| virtual_machine_ids | Map of VM name to the unique VM id (the compute fabric's GUID, not the resource id). |