Azure ACME Provisioner

Azure ACME Provisioner is a NodeJS package that provides necessary tools to automate the process of obtaining SSL/TLS certificates from ACME (Automatic Certificate Management Environment) compliant certificate authorities, such as Let's Encrypt, for applications hosted on Microsoft Azure. It uses Azure KeyVault to securely store and manage the obtained certificates and ACME account credentials. The package may function as a standalone tool, a docker image, as a library or as an Azure Function, making it versatile for various deployment scenarios.

Features

  • Uses ACME protocol to automate certificate issuance and renewal.
  • Stores ACME account information as secrets in Azure KeyVault for secure management.
  • Stores obtained SSL/TLS certificates in Azure KeyVault for easy access and management.
  • Automatically scans configured Azure DNS zones to identify records that require certificates (uses the acme tag to identify relevant recordsets).

Requirements

  • Node.js 24 or later
  • Azure subscription with:
    • Azure DNS zone(s) with records tagged acme: true or acme: enabled
    • Azure Key Vault instance
    • Managed Identity (or service principal) with permissions to read/write Key Vault secrets and certificates, and to manage DNS record sets

Installation

npm install azure-acme-provisioner

Or use the CLI directly via npx:

npx azure-acme-provisioner --help

DNS Zone Tagging

The provisioner discovers domains by scanning Azure DNS zones. Tag a zone or individual A/CNAME recordsets with acme: true to include them:

  • Zone-level tag — issues certificates for both the zone apex (example.com) and a wildcard (*.example.com) as a single SAN order.
  • Recordset-level tag — issues a certificate for that specific FQDN.

CLI Usage

Commands:
  run     Scan DNS zones and issue or renew certificates (default)
  scan    List all domains tagged for ACME management
  status  Show certificate expiry status for all managed domains
  renew    Force-renew a certificate for a specific domain
  download Download the PEM bundle for a domain from Key Vault

Common options:
  --keyvault-url <url>        Azure KeyVault URL
  --subscription-id <id>      Azure subscription ID
  --resource-group <rg>       Resource group to scan (repeatable)
  --dns-zone <zone>           Restrict to specific DNS zone (repeatable)
  --email <email>             ACME contact email
  --renewal-threshold <days>  Days before expiry to renew (default: 30)
  --dry-run                   Show what would be done without making changes
  --log-level <level>         debug | info | warn | error (default: info)
  --output <format>           table | json (scan and status commands)
  --http <port>               Use HTTP-01 challenge on the given port (run and renew only)

Challenge methods

By default run and renew use DNS-01 via Azure DNS (requires DNS Zone Contributor role).

Pass --http <port> to use HTTP-01 instead. The provisioner starts a temporary Express HTTP server on the given port and shuts it down after each certificate is issued. The server must be reachable from the internet on that port for the ACME CA to validate ownership.

# DNS-01 (default)
azure-acme-provisioner run

# HTTP-01 on port 80
azure-acme-provisioner run --http 80

# HTTP-01 on a non-privileged port (useful behind a reverse proxy or NAT rule)
azure-acme-provisioner run --http 8080

Note: Binding port 80 requires root privileges or CAP_NET_BIND_SERVICE. When running in Docker, map the host port to the container: -p 80:8080 and pass --http 8080.

Downloading certificates

The download command fetches the PEM bundle (private key + certificate + chain) from Key Vault and writes it to stdout or a file:

# Print to stdout
azure-acme-provisioner download api.example.com

# Write to a file
azure-acme-provisioner download api.example.com --output api.example.com.pem

Configuration

All configuration is via environment variables. CLI flags override env vars when both are provided.

Required

Variable Description
ACME_KEYVAULT_URL Azure Key Vault URL, e.g. https://myvault.vault.azure.net
ACME_SUBSCRIPTION_ID Azure subscription ID
ACME_RESOURCE_GROUPS Comma-separated list of resource groups to scan
ACME_CONTACT_EMAIL Contact email registered with the ACME CA

Optional

Variable Default Description
ACME_DNS_ZONES all zones in resource groups Comma-separated list of DNS zone names to restrict scanning
ACME_DIRECTORY_URL Let's Encrypt production ACME directory URL
ACME_RENEWAL_THRESHOLD_DAYS 30 Renew certificates this many days before expiry
ACME_DNS_PROPAGATION_WAIT 60 Maximum seconds to wait for DNS TXT record propagation
ACME_DNS_CHALLENGE_TTL 60 TTL (seconds) for DNS-01 challenge TXT records
ACME_HTTP_PORT unset If set to a positive integer, use HTTP-01 challenge on that port instead of DNS-01
ACME_LOG_LEVEL info Log level: debug, info, warn, error
ACME_SCHEDULE 0 0 2 * * * Azure Function timer schedule (cron expression, 6-field format). Only used when deployed as an Azure Function.

Azure Authentication

The provisioner uses DefaultAzureCredential from @azure/identity, which tries authentication methods in this order:

  1. Managed Identity — recommended for Azure-hosted deployments (Functions, ACI, AKS). Assign a system or user-assigned managed identity with the required RBAC roles. No credential configuration needed.
  2. Workload Identity Federation — for Kubernetes or CI/CD (GitHub Actions). Set AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_FEDERATED_TOKEN_FILE. No secrets required.
  3. Certificate-based service principal — set AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_CERTIFICATE_PATH (optionally AZURE_CLIENT_CERTIFICATE_PASSWORD, AZURE_CLIENT_SEND_CERTIFICATE_CHAIN).
  4. Client secret service principal — set AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET. Least secure; use only as a last resort.
  5. Azure CLI / Developer CLI — used automatically in local development when logged in via az login or azd auth login.

For sovereign clouds (Azure Government, Azure China), set AZURE_AUTHORITY_HOST to the appropriate authority endpoint.

Azure Function

The package includes an Azure Functions v4 timer trigger that runs the provisioner daily at 02:00 UTC.

The function app requires a Managed Identity with the following RBAC assignments:

Scope Role
Key Vault instance Key Vault Certificates Officer
Key Vault instance Key Vault Secrets Officer
Each DNS zone DNS Zone Contributor

Note: The only DNS changes made are temporary _acme-challenge.<domain> TXT records created during the DNS-01 challenge and deleted immediately after validation. No A, CNAME, or other records are modified. If you require tighter permissions than DNS Zone Contributor, create a custom role limited to Microsoft.Network/dnszones/TXT/write and Microsoft.Network/dnszones/TXT/delete.

Deploying with Azure Functions Core Tools

Prerequisites: Azure CLI, Azure Functions Core Tools v4, Node.js 24.

1. Log in to Azure

az login

2. Create a resource group and storage account (skip if they already exist)

az group create --name <resource-group> --location <location>

az storage account create \
  --name <storage-account-name> \
  --resource-group <resource-group> \
  --location <location> \
  --sku Standard_LRS

3. Create the Function App

az functionapp create \
  --name <function-app-name> \
  --resource-group <resource-group> \
  --storage-account <storage-account-name> \
  --consumption-plan-location <location> \
  --runtime node \
  --runtime-version 24 \
  --functions-version 4

4. Assign a system-assigned Managed Identity

az functionapp identity assign \
  --name <function-app-name> \
  --resource-group <resource-group>

Note the principalId from the output — you will need it in the next step.

5. Grant RBAC roles to the Managed Identity

# Key Vault Certificates Officer
az role assignment create \
  --assignee <principalId> \
  --role "Key Vault Certificates Officer" \
  --scope /subscriptions/<subscription-id>/resourceGroups/<kv-resource-group>/providers/Microsoft.KeyVault/vaults/<keyvault-name>

# Key Vault Secrets Officer
az role assignment create \
  --assignee <principalId> \
  --role "Key Vault Secrets Officer" \
  --scope /subscriptions/<subscription-id>/resourceGroups/<kv-resource-group>/providers/Microsoft.KeyVault/vaults/<keyvault-name>

# Option A — per zone (minimum permission, repeat for each managed DNS zone)
az role assignment create \
  --assignee <principalId> \
  --role "DNS Zone Contributor" \
  --scope /subscriptions/<subscription-id>/resourceGroups/<dns-resource-group>/providers/Microsoft.Network/dnszones/<zone-name>

# Option B — per resource group (convenient when all DNS zones are in one group)
az role assignment create \
  --assignee <principalId> \
  --role "DNS Zone Contributor" \
  --scope /subscriptions/<subscription-id>/resourceGroups/<dns-resource-group>

6. Configure production application settings

az functionapp config appsettings set \
  --name <function-app-name> \
  --resource-group <resource-group> \
  --settings \
    "ACME_KEYVAULT_URL=https://<keyvault-name>.vault.azure.net" \
    "ACME_SUBSCRIPTION_ID=<subscription-id>" \
    "ACME_RESOURCE_GROUPS=<dns-resource-group>" \
    "ACME_CONTACT_EMAIL=<email>" \
    "ACME_SCHEDULE=0 0 2 * * *"

7. Build and deploy

npm run build
func azure functionapp publish <function-app-name> --no-build

--no-build tells func to skip its own build step since we already compiled the TypeScript output. To also push application settings from local.settings.json in the same step, append --publish-local-settings --overwrite-settings. This is useful for the initial deployment or when settings and code change together.

Local testing

Create local.settings.json at the project root (gitignored) and fill in your values:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "ACME_KEYVAULT_URL": "https://<keyvault-name>.vault.azure.net",
    "ACME_SUBSCRIPTION_ID": "<subscription-id>",
    "ACME_RESOURCE_GROUPS": "<dns-resource-group>",
    "ACME_CONTACT_EMAIL": "<email>",
    "ACME_SCHEDULE": "0 0 2 * * *"
  }
}

Then run:

npm run build
func start

Docker

docker run --rm \
  -e ACME_KEYVAULT_URL=https://myvault.vault.azure.net \
  -e ACME_SUBSCRIPTION_ID=<subscription-id> \
  -e ACME_RESOURCE_GROUPS=my-rg \
  -e ACME_CONTACT_EMAIL=admin@example.com \
  ghcr.io/your-org/azure-acme-provisioner

When running in Azure Container Instances with a user-assigned Managed Identity, set AZURE_CLIENT_ID to the identity's client ID. No other credential variables are needed.

Library Usage

import { Provisioner, loadConfig } from 'azure-acme-provisioner';

const config = loadConfig(); // reads from environment variables
const provisioner = new Provisioner(config);
const result = await provisioner.run();
console.log(result);

Certificate Storage

Certificates are stored as native Azure Key Vault Certificates (PEM format, application/x-pem-file), making them available to Azure App Service, API Management, and other Azure services that integrate with Key Vault.

ACME account credentials (private key and account URL) are stored as Key Vault Secrets and reused across runs.

Certificate names are derived from the domain: dots are replaced with hyphens, wildcards become wildcard-, and a cert- prefix is added. For example:

Domain Key Vault certificate name
api.example.com cert-api-example-com
*.example.com cert-wildcard-example-com

AI Disclaimer

The files in this repository may contain code generated by AI tools. While we strive to ensure the quality and security of all code, we recommend reviewing any AI-generated code before using it in production environments.

S
Description
No description provided
Readme MIT 541 KiB
Languages
TypeScript 74.3%
JavaScript 24.4%
Dockerfile 1.3%