DEV Community

Cover image for Hardening Azure Acmebot for ISO 27001 & NIS2 Compliance with Terraform
david
david

Posted on • Originally published at woitzik.dev

Hardening Azure Acmebot for ISO 27001 & NIS2 Compliance with Terraform

Automating SSL/TLS certificates with Let's Encrypt and Azure Key Vault is a solved problem. Tools like Azure Acmebot make deployment incredibly simple.

In corporate environments targeting ISO 27001, KRITIS, or NIS2 compliance, "simple" is rarely sufficient. The standard Acmebot deployment relies on public endpoints — and a Storage Account or Key Vault reachable from the public internet will immediately trigger findings during a security audit.

Here's how to transition from a standard deployment to a fully network-isolated, Zero-Trust architecture — entirely managed with Terraform.

The Compliance Gap

Three components expose a public attack surface out of the box:

  1. Storage Account — accepts traffic from all networks by default
  2. Key Vault — operates with a public endpoint unless explicitly restricted
  3. Function App — the Acmebot dashboard is publicly reachable with no network-layer restriction

Target principle: Default-Deny at the network layer, not just the identity layer.

Step 1: VNet Integration

The Function App needs a dedicated subnet with a delegation to Microsoft.Web/serverFarms:

resource "azurerm_subnet" "acmebot_integration" {
  name                 = "snet-acmebot-integration"
  resource_group_name  = var.existing_vnet_rg
  virtual_network_name = var.existing_vnet_name
  address_prefixes     = ["10.0.1.0/27"]

  delegation {
    name = "delegation-acmebot"
    service_delegation {
      name    = "Microsoft.Web/serverFarms"
      actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Two settings that will cost you hours if you miss them:

site_config {
  vnet_route_all_enabled = true  # Forces ALL outbound through VNet
}

app_settings = {
  "WEBSITE_DNS_SERVER"      = "168.63.129.16"  # Azure internal DNS — required
  "WEBSITE_CONTENTOVERVNET" = "1"
}
Enter fullscreen mode Exit fullscreen mode

Without vnet_route_all_enabled = true, the Function still tries to reach Storage over the public internet and fails silently once you lock it down.

Step 2: Private Endpoints & DNS

The Storage Account requires four separate Private Endpoints — one each for blob, table, queue, and file. Azure Functions uses all four internally. Skip any one and the Function App deploys successfully but fails at runtime with cryptic storage errors:

resource "azurerm_private_endpoint" "storage_pe" {
  for_each  = toset(["blob", "table", "queue", "file"])
  name      = "pe-acmebot-st-${each.key}"
  subnet_id = azurerm_subnet.acmebot_endpoints.id

  private_service_connection {
    name                           = "psc-acmebot-st-${each.key}"
    private_connection_resource_id = azurerm_storage_account.acmebot_storage.id
    subresource_names              = [each.key]
    is_manual_connection           = false
  }

  private_dns_zone_group {
    name                 = "default"
    private_dns_zone_ids = [var.private_dns_zone_ids[each.key]]
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Default-Deny Firewall Rules

resource "azurerm_storage_account" "acmebot_storage" {
  public_network_access_enabled   = false
  allow_nested_items_to_be_public = false

  network_rules {
    default_action             = "Deny"
    bypass                     = ["AzureServices"]
    virtual_network_subnet_ids = [azurerm_subnet.acmebot_integration.id]
  }
}
Enter fullscreen mode Exit fullscreen mode

bypass = "AzureServices" is required — it allows Azure's internal control plane operations to continue working. Without it, diagnostic log shipping and other platform features silently break.

After applying this configuration, your Acmebot deployment meets the network isolation requirements of ISO 27001 Annex A.8, NIS2 Article 21, and KRITIS baseline controls.


The complete tested architecture including Entra ID automation and full Private Link configuration is on my blog and GitHub.

👉 Full article on woitzik.dev
👉 Free GitHub repo

Top comments (0)