A Terraform module to obtain a publicly trusted SSL certificate from the Let's Encrypt Certificate Authority using the ACME protocol (Automatic Certificate Management Environment). This module provides an Azure-friendly certificate issuance workflow, addressing the lack of a native equivalent to AWS Certificate Manager.
- Uses the vancluever/acme provider for true stateful certificate management in Terraform, backed by Lego as the ACME implementation.
- Seamless certificate renewal within 30 days of expiry.
- Automates DNS-01 challenges via the Lego azuredns provider.
- Handles Lego's requirement for authoritative NS records on the leaf zone. e.g. if you need a certificate for
www.private.example.combut only have a zone forexample.com, you can use CNAME redirection (see examples below). - CNAME redirection of DNS challenge records, including optional creation of corresponding CNAMEs in Azure Private DNS zones to satisfy Lego's local checks (wildcards supported).
- Stores the issued certificate in Azure Key Vault as a Certificate object.
- Also stores the certificate as a
.pfxfile in a base64-encoded Key Vault Secret with a strong randomised password, for compatibility with consumers that cannot use Certificate objects. - Supports multiple subscriptions (e.g. hub/spoke solution designs).
- Supports multiple Azure regions.
example.tfvars
acme_certificates = { # RESULT
simple = {
common_name = "*.example.com" # Wildcards are stripped
dns_challenge_zone_name = "example.com" # TXT _acme-challenge.example.com
}
redirected = {
common_name = "www.foo.example.com" # No Azure DNS zone for 'foo', use a CNAME to redirect DNS-01 challenge
dns_cname_zone_name = "example.com" # CNAME _acme-challenge.www.foo resolving to the below record
dns_challenge_zone_name = "acme.example.com" # TXT _acme-challenge.www.foo.acme.example.com
}
redirected_split_horizon = {
common_name = "*.bar.example.com" # In this example we also have a private DNS zone bar.example.com
dns_cname_zone_name = "example.com" # CNAME _acme-challenge.bar resolving to the below record
dns_challenge_zone_name = "acme.example.com" # TXT _acme-challenge.bar.acme.example.com
dns_private_cname_zone_name = "bar.example.com" # CNAME _acme-challenge resolving to the above record (satisfying Lego checks)
}
}
⚠️ dns_cname_zone_nameanddns_private_cname_zone_namemust match part ofcommon_name, whiledns_challenge_zone_namecan be any public zone you control.
certificate.tf
resource "acme_registration" "hub" {
email_address = var.acme_contact_email
}
module "acme_certificate" {
for_each = var.acme_certificates
source = "../../dtos-devops-templates/infrastructure/modules/acme-certificate"
providers = {
azurerm = azurerm
azurerm.dns_public = azurerm
azurerm.dns_private = azurerm
}
acme_registration_account_key_pem = acme_registration.hub.account_key_pem
certificate_naming_key = each.key
certificate = each.value
key_vaults = module.key_vault
private_dns_zone_resource_groups = azurerm_resource_group.private_dns_rg
public_dns_zone_resource_group_name = var.dns_zone_rg_name_public
subscription_id_dns_public = var.subscription_id_target
}To reference a resulting Key Vault Certificate:
module.acme_certificate["simple"].key_vault_certificate["uksouth"].versionless_secret_idTo reference a resulting .pfx certificate file:
module.acme_certificate["simple"].key_vault_certificate["uksouth"].pfx_blob_secret_name
module.acme.certificate["simple"].key_vault_certificate["uksouth"].pfx_passwordRemember to use the ACME staging API in your terraform providers block until the code works to your satisfaction.
Remove your test certificates from your Terraform state before switching accounts or before switching between API endpoints. By default the provider will try to revoke certificates with ACME on destroy, and if you accidentally delete or replace the account first this will result in a continual:
Error: acme: error: 400 :: POST :: https://acme-staging-v02.api.letsencrypt.org/acme/new-acct :: urn:ietf:params:acme:error:accountDoesNotExist :: No account exists with the provided key
which can only be resolved by manually pruning the certificates from the state file:
terraform state rm "module.acme_certificate[\"example\"].acme_certificate.this"For the list of inputs, outputs, resources... check the terraform module documentation.