Azure Functions-based dynamic DNS updater for Azure DNS zones.
This project provides a small HTTP endpoint that dynamic DNS clients (for example OpenWRT routers) can call to keep Azure DNS records current.
- Endpoint contract:
GET /api/update?client=<name>&key=<raw-key>&zone=<zone>&name=<record>[&ip=<address>] - Authentication: client name + raw key (validated against SHA-256 hash in config)
- Authorization: per-client allowed zone/record list (
*wildcard record name supported) - Record behavior: updates only the matching address family
- IPv4 ->
Arecord only - IPv6 ->
AAAArecord only
- IPv4 ->
- Runtime: .NET 8 isolated Azure Functions
- Hosting target: Azure Functions Flex Consumption (Linux)
- DNS backend: Azure DNS via Azure SDK and managed identity
- Config source: packaged file
config/dyndns.json(by design, to keep complexity low)
- Function receives
GET /api/updaterequest. - Query parameters are validated (
client,key,zone,name; optionalip). config/dyndns.jsonis loaded.- Client is authenticated by comparing SHA-256 hash of provided key.
- Requested record is authorized for that client.
- Effective IP is resolved:
ipquery value if provided and valid- otherwise source IP from connection
- DNS update is sent to Azure DNS using managed identity.
- Plain-text response is returned for DDNS client compatibility.
Responses are plain text and stable for client compatibility:
- Success:
OK: ...with HTTP200 - Validation failures:
ERROR: ...with HTTP400 - Invalid credentials: HTTP
401 - Unauthorized record: HTTP
403 - Azure DNS backend failure: HTTP
502 - Server/configuration failure: HTTP
500
src/AzureDdns.FunctionApp- Function app codeFunctions/UpdateDnsFunction.cs- HTTP entrypoint and orchestrationServices/AuthService.cs- authentication + authorization checksServices/IpResolver.cs- source/explicit IP handlingServices/DnsUpdateService.cs- Azure DNS SDK update logicServices/ConfigProvider.cs- reads DDNS config JSONconfig/dyndns.json- sample DDNS configurationlocal.settings.json.example- local app settings template
tests/AzureDdns.FunctionApp.Tests- xUnit testsscripts/smoke-test.ps1- post-deployment smoke test for end-to-end DDNS validationinfra/main.bicep- infrastructure definitioninfra/modules/dns-zone-rbac.bicep- optional zone-scoped RBAC assignment moduleinfra/main.parameters.json- deploy-time parameter valuesdocs/deployment-plan.md- detailed deployment + validation runbook.azure/plan.md- preparation plan artifact for Azure workflow
| Setting | Required | Purpose |
|---|---|---|
DNS_SUBSCRIPTION_ID |
Yes | Subscription containing target Azure DNS zones |
DNS_RESOURCE_GROUP |
Yes | Resource group containing target Azure DNS zones |
CONFIG_PATH |
Yes | Relative/absolute path to DDNS config file; default is config/dyndns.json |
AZURE_FUNCTIONS_ENVIRONMENT |
Recommended | Environment label (Development, Production, etc.) |
APPLICATIONINSIGHTS_CONNECTION_STRING |
Recommended | Application Insights connection |
AzureWebJobsStorage |
Required in Azure | Functions host storage connection |
LOG_ALL_REQUEST_HEADERS_FOR_IP_DIAGNOSTICS |
Yes | Enables/disables IP header logging' default is false. |
Schema summary:
zones-> dictionary keyed by zone name- each zone supports
ttl
- each zone supports
clients-> list of authenticated callersname-> client identifierkeyHash-> SHA-256 hex hash of raw keyallowedRecords-> list of{ zone, name }permissions
Security notes:
- Store only hashed keys, never raw keys.
- Keep raw keys out of source control and logs.
- Rotate keys by updating
keyHashand redeploying app package.
- Copy
src/AzureDdns.FunctionApp/local.settings.json.exampletosrc/AzureDdns.FunctionApp/local.settings.json. - Set local values for:
DNS_SUBSCRIPTION_IDDNS_RESOURCE_GROUPCONFIG_PATH(normallyconfig/dyndns.json)
- Update
src/AzureDdns.FunctionApp/config/dyndns.jsonwith your test zone/client entries and hashed keys. - Build:
dotnet build
- Run tests:
dotnet test
- Run function locally (from app project folder) and send a test request to
/api/update.
Use these steps when deploying without GitHub Actions.
- Install required tooling:
- Azure CLI (
az) - .NET 8 SDK
- Azure Functions Core Tools v4 (for local verification)
- Azure CLI (
- Sign in to Azure CLI:
az login
- Select the subscription where infrastructure will be deployed:
az account set --subscription <infra-subscription-id-or-name>
- Confirm the target resource group exists (or create it):
az group create --name <resource-group> --location <azure-region>
- Clone the repository and switch to the intended branch:
git clone https://github.com/arkane-systems/azure-ddns.gitcd azure-ddnsgit checkout <branch-or-tag>
- Restore dependencies and validate baseline build:
dotnet restoredotnet build
- Optionally run tests before deploying:
dotnet test
- Open
infra/main.parameters.json. - Set required values for your environment:
baseNameenvironmentNamelocationdnsSubscriptionIddnsResourceGroup
- Decide whether to use explicit resource names or derived names:
- Leave optional name parameters empty to use deterministic generated names.
- Or set explicit values for
functionAppName,storageAccountName,appInsightsName,logAnalyticsWorkspaceName, andfunctionPlanName.
- If you want zone-scoped DNS RBAC created by Bicep, populate
dnsZoneNameswith the zone names that this app will update. - Save the file.
- Open
src/AzureDdns.FunctionApp/config/dyndns.json. - Update the
zonessection with your real zone names and desired TTL values. - Update the
clientssection for your local requirements:- Set each client
name. - Replace each
keyHashwith the SHA-256 hex hash of that client’s raw key. - Set
allowedRecordsto the exact{ zone, name }pairs each client is allowed to update (use*for wildcard record-name authorization within a zone when needed).
- Set each client
- Ensure no raw client keys are stored in source files.
- Save the file.
- Deploy
infra/main.bicepusing the updated parameters file:az deployment group create --resource-group <resource-group> --template-file infra/main.bicep --parameters @infra/main.parameters.json
- (Optional) Run a what-if preview before deployment:
az deployment group what-if --resource-group <resource-group> --template-file infra/main.bicep --parameters @infra/main.parameters.json
- Record outputs and final resource names (especially Function App and Storage resources).
After infrastructure deployment, set required app settings on the Function App:
DNS_SUBSCRIPTION_ID= subscription that contains the DNS zones.DNS_RESOURCE_GROUP= resource group that contains the DNS zones.CONFIG_PATH=config/dyndns.json(unless you intentionally changed the location).
Example:
az functionapp config appsettings set --name <function-app-name> --resource-group <resource-group> --settings DNS_SUBSCRIPTION_ID=<dns-subscription-id> DNS_RESOURCE_GROUP=<dns-resource-group> CONFIG_PATH=config/dyndns.json
- Publish the Function App:
dotnet publish src/AzureDdns.FunctionApp/AzureDdns.FunctionApp.csproj -c Release -o out/functionapp
- Create a deployment zip from the publish output.
- Deploy the zip package to the target Function App:
az functionapp deployment source config-zip --name <function-app-name> --resource-group <resource-group> --src <path-to-zip>
- Confirm the Function App is running and responds on
/api/update. - Verify managed identity role assignments are present as expected (including optional DNS zone-scoped RBAC when enabled).
- Run the smoke test:
./scripts/smoke-test.ps1 -FunctionBaseUrl 'https://<function-app-name>.azurewebsites.net' -ClientName '<client>' -Zone '<zone>' -Name '<record>'
- Confirm behavior:
- IPv4 requests update only
A. - IPv6 requests update only
AAAA. - Unauthorized updates are rejected with expected status codes.
- IPv4 requests update only
- Infrastructure changes: update
infra/main.bicepand/orinfra/main.parameters.json, then rerun the Bicep deployment command. - Runtime policy/client changes: update
src/AzureDdns.FunctionApp/config/dyndns.json, republish, and redeploy the app package. - Always rerun smoke validation after either type of change.
If desired, validate behavior locally first:
- Copy
src/AzureDdns.FunctionApp/local.settings.json.exampletosrc/AzureDdns.FunctionApp/local.settings.json. - Set local values for
DNS_SUBSCRIPTION_ID,DNS_RESOURCE_GROUP, andCONFIG_PATH. - Run the app locally and test the
/api/updateendpoint with your configured client credentials.
After deployment, verify:
- Managed identity exists on the Function App.
- Identity has expected role assignments:
- storage access for deployment container
- optional DNS Zone Contributor on each configured DNS zone
- IPv4 update requests modify only
Arecords. - IPv6 update requests modify only
AAAArecords. - Unauthorized client/record requests are rejected.
- Logs contain useful context without exposing raw keys.
Use scripts/smoke-test.ps1 after deployment to perform an end-to-end functional validation of the Function App and Azure DNS integration.
What the script does:
- Accepts the target Function App URL plus the client, zone, and record name to test.
- Generates one random IPv4 address from the RFC5737 documentation ranges.
- Generates one random IPv6 address from the RFC3849 documentation range.
- Sends an IPv4 update request and verifies the plain-text success response.
- Queries an authoritative name server for the target zone and waits for the
Arecord to match the generated IPv4 address. - Sends an IPv6 update request and verifies the plain-text success response.
- Queries an authoritative name server again and waits for the
AAAArecord to match the generated IPv6 address. - Confirms that the earlier
Arecord value remains unchanged to validateA/AAAAindependence.
Parameters:
-FunctionBaseUrl- Base URL of the deployed Function App, with or without/api/update-ClientName- DDNS client name configured inconfig/dyndns.json-ClientKey- raw DDNS client key; if omitted, the script usesAZURE_DDNS_CLIENT_KEY-Zone- DNS zone to test-Name- relative record name to test (@for zone apex)-DnsTimeoutSeconds- optional DNS propagation wait timeout; default120-DnsPollIntervalSeconds- optional poll interval between authoritative DNS checks; default5
Example:
$env:AZURE_DDNS_CLIENT_KEY = 'your-raw-client-key'
.\scripts\smoke-test.ps1 `
-FunctionBaseUrl 'https://<your-function-app>.azurewebsites.net' `
-ClientName 'stargate' `
-Zone 'arkane-systems.net' `
-Name 'smoke'Prerequisites and notes:
- The tested record must already be authorized for the selected client in
config/dyndns.json. - The script depends on
Resolve-DnsNamebeing available in PowerShell. - The script verifies authoritative DNS results, so completion time depends on Azure DNS write latency and name server visibility.
- The generated addresses are intentionally from documentation-only ranges so the smoke test never points records at real client endpoints.
If you revisit this repo after a long gap, start in this order:
README.md(high-level model + references)docs/deployment-plan.md(exact deploy/validate procedure)infra/main.bicep(what is provisioned and why)src/AzureDdns.FunctionApp/Functions/UpdateDnsFunction.cs(runtime request flow)