A webhook provider for ExternalDNS that manages DNS records in a UniFi Network controller. ExternalDNS keeps DNS in sync with your Kubernetes Ingresses and Services. This provider writes those records to the built-in DNS of UniFi through the static-dns API of the Network application.
| Component | Minimum version |
|---|---|
| ExternalDNS | v0.21.0 |
| UniFi OS | 5.x |
| UniFi Network | 9.0.0 |
The provider runs as a sidecar next to the ExternalDNS controller. It speaks the ExternalDNS webhook protocol on one side and the UniFi Network API on the other. Records go through /proxy/network/v2/api/site/{site}/static-dns. At startup, the provider lists that path once, so a bad host, a bad key, or an unknown site fails before the first reconcile. Ubiquiti does not document the static-dns endpoints. Unlike the DNS Policies endpoints of the Integration API, they accept names whose labels start with an underscore, for example the RFC 9462 DDR name _dns.resolver.arpa.
The provider reaches UniFi in one of two ways:
- Local (default): the provider connects directly to the controller on your network.
- Cloud connector: the provider sends requests through
api.ui.comto a console that you cannot reach on the LAN. See Cloud connector.
The ExternalDNS controller applies domain filters, not this webhook. See Domain filtering.
UniFi uses dnsmasq as its DNS backend, so the provider has the same limits:
- UniFi does not support wildcard names such as
*.example.com. - UniFi permits one CNAME per name. If the webhook creates a CNAME where one already exists, it deletes the existing record first. If ExternalDNS sends more than one target for one CNAME, the webhook uses only the first target. It drops the other targets and logs a warning.
Every request authenticates with an API key. The provider does not support username and password authentication.
For a local controller, log into your console by IP. Go to Settings → Control Plane → Integrations → Create API Key. Copy the key.
Only a Super Admin can create an API key. The key continues to work after you downgrade the user. For least privilege, create a dedicated
external-dnsuser. While this user is a Super Admin, generate its key. Then change the role of the user to Site Admin. A Site Admin can manage DNS records.
For the cloud connector, create an account-level key in the UniFi Site Manager under account settings → API. This key is not the same as a local key for one console.
apiVersion: v1
kind: Secret
metadata:
name: unifi-dns-secret
stringData:
UNIFI_API_KEY: <your-api-key>Add the ExternalDNS chart repository:
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/Create a values file (external-dns-unifi-values.yaml):
fullnameOverride: external-dns-unifi
provider:
name: webhook
webhook:
image:
repository: ghcr.io/home-operations/external-dns-unifi-webhook
tag: main # replace with a versioned release tag
env:
- name: UNIFI_HOST
value: https://unifi.internal # your UniFi controller, or https://api.ui.com for the cloud connector
- name: UNIFI_API_KEY
valueFrom:
secretKeyRef:
name: unifi-dns-secret
key: UNIFI_API_KEY
livenessProbe:
httpGet:
path: /healthz
port: http-webhook
initialDelaySeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /readyz
port: http-webhook
initialDelaySeconds: 10
timeoutSeconds: 5
triggerLoopOnEvent: true
policy: sync
sources:
- gateway-httproute
- service
txtOwnerId: main
txtPrefix: k8s.main.%{record_type}-
domainFilters:
- example.com # replace with your domain
serviceMonitor:
enabled: trueInstall:
helm install external-dns-unifi external-dns/external-dns \
-f external-dns-unifi-values.yaml --version 1.21.1 -n external-dnsSee the chart values for additional options.
| Variable | Description | Default |
|---|---|---|
UNIFI_HOST |
Controller address, or https://api.ui.com for the cloud connector (required). |
— |
UNIFI_API_KEY |
API key used to authenticate (required). Local and cloud keys differ — see below. | — |
UNIFI_SITE |
Site name. See Finding the site name. | default |
UNIFI_CONSOLE_ID |
Console ID. Setting this routes requests through the api.ui.com cloud connector. |
— |
UNIFI_SKIP_TLS_VERIFY |
Skip TLS verification. Ignored in cloud mode. | true |
UNIFI_CA_CERT |
Path to a PEM bundle of extra trusted CAs (alternative to skipping verification). | — |
UNIFI_APPLY_WORKERS |
Maximum concurrent record operations during a reconcile. | 5 |
UNIFI_RETRY_ATTEMPTS |
Total attempts per request (including the first). | 3 |
UNIFI_RETRY_INITIAL_DELAY |
Initial backoff before the first retry. | 500ms |
UNIFI_RETRY_MAX_DELAY |
Maximum backoff between retries (also caps Retry-After). |
10s |
UNIFI_REQUEST_TIMEOUT |
Per-attempt timeout for controller requests. Unset disables it (see below). | — |
The site name is the short name that the Network application uses in its URLs, not the display name in the site switcher. For most setups it is default. If you have more than one site, open the site in the Network application and read the <name> segment of the browser URL:
- UniFi OS console:
https://<console>/network/<name>/dashboard - Site Manager:
https://unifi.ui.com/consoles/<console-id>/network/<name>/dashboard - Standalone Network Application:
https://<host>:8443/manage/site/<name>/dashboard
ExternalDNS gives each call to the webhook a fixed time budget: --webhook-provider-read-timeout plus --webhook-provider-write-timeout (5s + 10s by default). If the webhook does not answer within this budget, ExternalDNS treats the call as a fatal error and exits. If the webhook returns an HTTP 5xx, ExternalDNS only logs a soft error. Without a timeout per attempt, a controller that accepts the connection and then stalls keeps the webhook waiting past that budget. ExternalDNS then restarts instead of a retry on the next sync.
Set UNIFI_REQUEST_TIMEOUT to limit each attempt. If an attempt times out before the controller answers, the webhook retries GET and DELETE requests as it does for other network errors. A stall after the response headers fails without a retry. In both cases, the webhook answers 500 after it gives up. Keep UNIFI_RETRY_ATTEMPTS × UNIFI_REQUEST_TIMEOUT plus the backoff between attempts below the ExternalDNS budget. With the defaults (3 attempts, 500ms initial delay, up to 50% jitter), a 4s timeout gives a worst case of about 14.3s, which is below the 15s default. As an alternative, raise the ExternalDNS timeouts. Keep their sum below SERVER_WRITE_TIMEOUT.
To manage a remote console without a controller exposed on your LAN, set UNIFI_CONSOLE_ID. Set UNIFI_HOST to https://api.ui.com. The webhook then sends requests through /v1/connector/consoles/{consoleId}/proxy/network/.... The API is the same as for a local connection. Only the routing prefix changes.
- Use an account-level API key from the UniFi Site Manager, not a local key for one console.
- Find your console ID with the Site Manager API (
GET https://api.ui.com/v1/hosts) or in the console URL at unifi.ui.com. - The webhook ignores
UNIFI_SKIP_TLS_VERIFY.api.ui.compresents a publicly-trusted certificate. If the webhook skipped verification, it exposed your API key. - Reconciliation depends on the availability and the rate limits of the Ubiquiti API. The built-in backoff handles
429responses, but a local connection is better when you can reach the controller. - The connector forwards the
static-dnspaths today. Ubiquiti stated that the connector is meant to reach only the public integration API of each application, and on 2026-11-05 it refused those paths for a few hours. If that refusal becomes permanent, cloud mode stops until this webhook adapts.
Configure --domain-filter (and its variants) on the ExternalDNS controller, not on this webhook. UniFi has no zones that the webhook can filter by, so the webhook follows the ExternalDNS GetDomainFilter contract and leaves the filter to the controller.
| Variable | Description | Default |
|---|---|---|
SERVER_HOST |
Address that the webhook server binds to. | localhost |
SERVER_PORT |
Webhook server port. | 8888 |
SERVER_READ_TIMEOUT |
Request read timeout. | 60s |
SERVER_READ_HEADER_TIMEOUT |
Read-header timeout (Slowloris mitigation). | 5s |
SERVER_WRITE_TIMEOUT |
Response write timeout. | 60s |
SERVER_IDLE_TIMEOUT |
Keep-alive idle timeout. | 120s |
SERVER_MAX_HEADER_BYTES |
Maximum request header size. | 65536 |
SERVER_MAX_BODY_BYTES |
Maximum POST body size before returning 413. |
5242880 (5 MiB) |
HEALTH_SERVER_ADDR |
Address for the /metrics, /healthz, /readyz server. |
:8080 |
READINESS_CACHE_TTL |
How long /readyz caches the upstream probe result. |
30s |
PPROF_ENABLED |
Mount /debug/pprof/* on the health server (not in prod). |
false |
LOG_LEVEL |
Log verbosity: debug, info, warn, error. |
info |
LOG_FORMAT |
Set to text for human-readable output instead of JSON. |
JSON |
| Endpoint | Port | Purpose |
|---|---|---|
/ |
8888 |
ExternalDNS negotiate (returns the provider media type). |
/records |
8888 |
ExternalDNS GET (list) and POST (apply changes). |
/healthz |
8888, 8080 |
Liveness — 200 OK while the process is running. |
/readyz |
8888, 8080 |
Readiness — probes UniFi, cached for READINESS_CACHE_TTL. |
/metrics |
8080 |
Prometheus metrics. |
/debug/pprof/ |
8080 |
Go pprof endpoints (only when PPROF_ENABLED=true). |
The webhook serves /healthz and /readyz on both ports. Kubernetes probes can therefore target the webhook port directly. You do not have to expose a second container port through the chart.
The move to UniFi Network 10.3.58 introduced breaking changes:
| Setting | Change |
|---|---|
UNIFI_EXTERNAL_CONTROLLER |
Removed. Point UNIFI_HOST at the controller directly, or use the cloud connector for remote consoles. |
DOMAIN_FILTER, EXCLUDE_DOMAIN_FILTER, REGEXP_DOMAIN_FILTER, REGEXP_DOMAIN_FILTER_EXCLUSION |
Removed. Configure --domain-filter on the ExternalDNS controller instead (see Domain filtering). |
LOG_FORMAT=test |
Renamed to LOG_FORMAT=text. |
The move of records to the static-dns endpoints changed the operation label value on externaldns_webhook_unifi_retries_total and externaldns_webhook_unifi_rate_limits_total from dns_policies to static_dns. Update any alert or dashboard selectors that match the old value.
The provider no longer uses the Integration API. UNIFI_SITE must be the site name (see Finding the site name). A site UUID from the Integration API fails validation at startup. The minimum UniFi Network version is 9.0.0, the first release with API keys. The static-dns endpoints are tested on Network 10.x only; older releases that accept an API key are untested.
Thanks to everyone in the Home Operations Discord community.