Skip to content

Repository files navigation

Terraform Azure Machine Configuration

Onboard machines to Azure Machine Configuration (the modern, cross platform successor to Azure Automation State Configuration, which retires on 2027-09-30) and audit or enforce PowerShell DSC in guest state. Assign the built in compute security and CIS baselines across a scope, and apply custom DSC packages to individual Windows and Linux VMs with continuous drift correction (ApplyAndAutoCorrect). Windows uses PSDSC v2, Linux uses PSDSC v3, and the same model covers Azure and Arc enabled servers.

CI Release Terraform Registry License


What it does

  • Onboarding. Installs the Guest Configuration extension (AzurePolicyforWindows / AzurePolicyforLinux) on the VMs you name, so per machine assignments actually execute. The VM must carry a system assigned identity (the Libre DevOps VM modules set one by default).
  • Built in baselines at scale. A curated builtin catalog (verified live) resolves friendly keys (windows_cis, linux_cis, windows_compute_baseline, linux_compute_baseline, prereq_system_assigned) to the right built in definition or initiative, assigned across a resource group, subscription, or management group. Audit by default; enforce is opt in.
  • Custom DSC packages per machine. Point a machine at a custom package (content_uri plus the UPPERCASE SHA256 content_hash) and choose Audit, ApplyAndMonitor, or ApplyAndAutoCorrect. Build the packages with the LibreDevOpsHelpers Machine Configuration helpers (which wrap the GuestConfiguration PowerShell module), as the complete example and CI do.
  • Remediation wiring. Assignments that deploy (DeployIfNotExists) get a system assigned identity and the role grants they need.

Usage

module "machine_configuration" {
  source = "libre-devops/machine-configuration/azurerm"

  # Onboard a VM (install the Guest Configuration extension).
  guest_configuration_extensions = {
    AzurePolicyforLinux = {
      virtual_machine_id = module.linux_vm.virtual_machine_ids["vm-lnx-ldo-uks-dev-01"]
      os_type            = "Linux"
    }
  }

  # Audit the Linux CIS baseline across the resource group.
  policy_assignments = {
    "mc-linux-cis" = {
      display_name = "Linux CIS baseline (audit)"
      scope_type   = "resource_group"
      scope_id     = module.rg.ids["rg-ldo-uks-dev-01"]
      location     = "uksouth"
      builtin      = "linux_cis"
    }
  }
}

Examples

Developing

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.

Security scan exceptions

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 the table below so the reason is auditable.

Trivy ID Resource Finding Justification
AVD-AZU-0013 examples/complete key vault Vault network ACL does not block by default Disposable example vault, created + written + read + destroyed in one apply, so it cannot be IP allow-listed before it exists. Public network Allow lets the CI runner write the demo secrets without a per-run firewall dance (the Libre DevOps example convention, matching the linux-vm module). A real vault stays firewalled (module default) and uses the action's allow-list dance. The module creates no key vault.

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. Where the finding is out of this module's scope, point the justification at the Libre DevOps module that does address it (for example the private-endpoint module). Both the file and this table are reviewed in the pull request.

Reference

The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.

Requirements

Name Version
terraform >= 1.9.0, < 2.0.0
azurerm >= 4.0.0, < 5.0.0

Providers

Name Version
azurerm >= 4.0.0, < 5.0.0

Modules

No modules.

Resources

Name Type
azurerm_management_group_policy_assignment.this resource
azurerm_policy_virtual_machine_configuration_assignment.this resource
azurerm_resource_group_policy_assignment.this resource
azurerm_role_assignment.remediation resource
azurerm_subscription_policy_assignment.this resource
azurerm_virtual_machine_extension.guest_config resource

Inputs

Name Description Type Default Required
guest_configuration_extensions Guest Configuration VM extensions to install, keyed by a stable name. Installing the
extension is the onboarding step: a per machine configuration assignment will not execute
until the extension is present on the virtual machine, and the virtual machine must carry a
system assigned identity so the platform can report and remediate. Set os_type to Windows or
Linux; the module picks the correct publisher and type per OS when they are not overridden.
map(object({
virtual_machine_id = string
os_type = string
type_handler_version = optional(string)
auto_upgrade_minor_version = optional(bool, true)
automatic_upgrade_enabled = optional(bool, true)
settings = optional(string)
protected_settings = optional(string)
tags = optional(map(string))
}))
{} no
machine_configuration_assignments Per machine (guest configuration) assignments, keyed by a stable name. Onboard a specific
virtual machine to a built in baseline (for example AzureWindowsBaseline or AzureLinuxBaseline,
leaving content_uri and content_hash unset) or to a custom DSC package (both content_uri and
content_hash required; content_hash is the UPPERCASE SHA256 of the .zip; the service requires a
version for custom packages, so the module defaults it to 1.0.0 when unset). assignment_type
defaults to Audit (the safe default); ApplyAndAutoCorrect enforces continuously. The referenced
virtual machine must already carry the Guest Configuration extension (see
guest_configuration_extensions) and a system assigned identity.
map(object({
name = string
virtual_machine_id = string
location = string
assignment_type = optional(string, "Audit")
version = optional(string)
content_uri = optional(string)
content_hash = optional(string)
parameters = optional(map(string), {})
}))
{} no
policy_assignments Scoped Guest Configuration policy assignments, keyed by a stable name: the fleet wide way to
audit or enforce baselines across every machine in a resource group, subscription or management
group. Point each entry at the curated built in catalog with builtin (for example linux_cis,
windows_cis, linux_compute_baseline, windows_compute_baseline, prereq_system_assigned), or set
policy_definition_id / policy_set_definition_id explicitly. Catalog definitions with required
parameters are defaulted by the module (linux_cis: BaselineSettings, an empty string, the stock
CIS baseline); anything in parameters overrides the default per key. enforcement_mode defaults to Default;
set it to DoNotEnforce for report only. A system assigned identity plus remediation role grants
are wired for definitions that deploy (DeployIfNotExists).
map(object({
display_name = string
description = optional(string)
scope_type = string
scope_id = string
location = string
builtin = optional(string)
policy_definition_id = optional(string)
policy_set_definition_id = optional(string)
parameters = optional(any)
enforcement_mode = optional(string, "Default")
not_scopes = optional(list(string), [])
identity_type = optional(string, "SystemAssigned")
identity_ids = optional(list(string), [])

remediation_role_definition_ids = optional(list(string), [])
remediation_role_scope = optional(string)
}))
{} no

Outputs

Name Description
builtin_catalog Curated map of friendly key to built in Guest Configuration definition/initiative id. Reference a key via policy_assignments[*].builtin, or read the id directly.
builtin_catalog_keys The valid keys of builtin_catalog (the accepted values for policy_assignments[*].builtin).
guest_configuration_extension_ids_zipmap Map of extension key to { name, id } for easy composition.
guest_configuration_extensions Map of extension key to the installed Guest Configuration extension (id, name, type, virtual_machine_id).
machine_configuration_assignment_ids_zipmap Map of assignment key to { name, id } for easy composition.
machine_configuration_assignments Map of assignment key to the per machine Guest Configuration assignment (id, name, virtual_machine_id).
policy_assignment_ids_zipmap Map of assignment key to { name, id } across every scope, for easy composition.
policy_assignment_principal_ids Map of assignment key to its system assigned identity principal id (null when identity is None).
policy_assignments Map of assignment key to the scoped policy assignment (id, name, scope_type, principal_id of its identity).
remediation_role_assignment_ids Map of ':' to the created role assignment id.

About

🛡️ Terraform module for Azure Machine Configuration: onboard Windows/Linux VMs, assign CIS + compute security baselines, and enforce custom DSC packages with drift correction

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages