# AdguardHome provider for ExternalDNS A [webhook plugin](https://github.com/kubernetes-sigs/external-dns/blob/v0.18.0/docs/tutorials/webhook-provider.md) for [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to support AdguardHome DNS provider. This provider implementation is based on using AdguardHome [filtering rules](https://adguard-dns.io/kb/general/dns-filtering-syntax/). It takes ownership only for rules which are created by this provider, so existing rules are not touched. ### Compatibility This plugin was tested with AdguardHome up to v0.107.79 and ExternalDNS v0.24.0. The provider uses the ExternalDNS v0.24.0 webhook library and requires Go 1.27.0 or later to build. The webhook server listens on `:8888` with 10-second read and write timeouts and a default 32 MiB request body limit. Oversized requests return HTTP 413. Pass `--max-body-size` to the provider to override the limit in bytes, for example `--max-body-size=67108864` for 64 MiB. Values of `0` or less disable the limit; prefer a finite limit to bound memory use. Before accepting webhook requests, the provider checks AdGuard and migrates managed rules when needed. Startup failures leave the webhook unavailable. SIGINT and SIGTERM cancel in-flight AdGuard HTTP calls and stop the process. ## Experimental DNS rewrite rules Set `ADGUARD_HOME_USE_DNSREWRITE=true` on the provider container to test explicit [`$dnsrewrite` rules](https://adguard-dns.io/kb/general/dns-filtering-syntax/#dnsrewrite-modifier). The default is `false`; an unset or empty value also disables the flag. Invalid boolean values fail startup. Restart the provider after changing the flag. When enabled, the provider migrates its existing managed A and TXT records at startup, including when ExternalDNS has no DNS changes to apply. New records also use this syntax. Managed allow-rule companions are removed. Labels and rules belonging to other owners remain unchanged. TXT values, including ExternalDNS TXT registry ownership records, become queryable over DNS. Migration does not write anything in provider dry-run mode. Wildcard hostnames are rejected in rewrite mode; exclude them before enabling it. Rejection prevents startup migration or the entire change batch from being saved. For example, a managed record becomes two adjacent lines: ```text ! 192.0.2.10 app.example.com #$managed by external-dns |app.example.com|$dnsrewrite=NOERROR;A;192.0.2.10 ``` Keep each ownership comment adjacent to its rule; missing or mismatched pairs stop reconciliation. Rules match only the exact non-wildcard hostname. Multiple addresses produce one pair per address. TXT pairs preserve the original value and labels in a JSON comment: ```text ! $managed by external-dns {"dnsName":"txt.example.com","targets":["verification-token"],"recordType":"TXT"} |txt.example.com|$dnstype=TXT,dnsrewrite=NOERROR;TXT;verification-token ``` TXT targets can be plain text or a single DNS-quoted string, with at most 255 decoded bytes per target. Reserved characters are escaped automatically; original values and labels are preserved for rollback. Multiple targets produce separate TXT records. Invalid quoting, multi-string quoted targets, oversized values, and literal line breaks reject the entire batch without saving rules. During startup, one invalid existing TXT target prevents migration and the webhook from starting. A and TXT records can share a hostname. TXT-only rewrites do not suppress upstream A, AAAA, or other record types. AdGuard Home classifies these answers as **Rewritten**, not Blocked, so they do not inflate blocked-query statistics. This requires AdGuard Home v0.105.0 or later. An A-only rewrite returns an empty `NOERROR` answer for other query types unless another applicable rewrite supplies that type. Provider management markers and labels stay in comments; only endpoint TXT values are served. With the flag off, TXT values remain comments and are not served over DNS. To roll back, set the flag to `false` or remove it, then restart this build of the provider. Startup converts managed A rewrite pairs back to hosts-style rules, restores their allow-rule companions, and converts TXT pairs back to comments. Roll back before switching to an older provider build, which cannot read the new metadata pairs. Older builds also misparse TXT values containing whitespace, even after rollback. Remove those TXT endpoints from desired state and reconcile before downgrading, or keep this build with the flag disabled. ## Setting up ExternalDNS for AdguardHome This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using AdguardHome. ### Deploy ExternalDNS Connect your `kubectl` client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS. ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: external-dns --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: external-dns rules: - apiGroups: [""] resources: ["services","endpoints","pods","nodes"] verbs: ["get","watch","list"] - apiGroups: ["extensions","networking.k8s.io"] resources: ["ingresses"] verbs: ["get","watch","list"] - apiGroups: [ "discovery.k8s.io" ] resources: [ "endpointslices" ] verbs: [ "get","watch","list" ] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: external-dns-viewer roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: external-dns subjects: - kind: ServiceAccount name: external-dns namespace: default --- apiVersion: apps/v1 kind: Deployment metadata: name: external-dns spec: replicas: 1 selector: matchLabels: app: external-dns strategy: type: Recreate template: metadata: labels: app: external-dns spec: serviceAccountName: external-dns containers: - name: external-dns image: registry.k8s.io/external-dns/external-dns:v0.18.0 args: - --source=service # ingress is also possible - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - --provider=webhook - --webhook-provider-url=http://localhost:8888 - name: adguardhome-provider image: ghcr.io/zekker6/external-dns-provider-adguard:v1.5.2 env: - name: ADGUARD_HOME_URL value: "YOUR_ADGUARD_HOME_URL" # Note: URL should be in the format of http://adguard.home:3000/control/ - name: ADGUARD_HOME_PASS value: "YOUR_ADGUARD_HOME_PASSWORD" - name: ADGUARD_HOME_USER value: "YOUR_ADGUARD_HOME_USER" # It is possible to run multiple instances of provider with a single AdguardHome instance by using different owner refs # - name: ADGUARD_HOME_MANAGED_BY_REF # value: "cluster-name" ``` ### Deploy ExternalDNS with the Helm chart ExternalDNS project provides an [official Helm chart](https://github.com/kubernetes-sigs/external-dns/tree/master/charts/external-dns) that can be used to deploy ExternalDNS with AdguardHome provider. Example values file for the Helm chart: ```yaml sources: - ingress - service provider: name: webhook webhook: image: repository: ghcr.io/zekker6/external-dns-provider-adguard tag: v1.5.2 pullPolicy: IfNotPresent env: - name: ADGUARD_HOME_URL valueFrom: secretKeyRef: name: adguard-credentials key: ADGUARD_HOME_URL - name: ADGUARD_HOME_USER valueFrom: secretKeyRef: name: adguard-credentials key: ADGUARD_HOME_USER - name: ADGUARD_HOME_PASS valueFrom: secretKeyRef: name: adguard-credentials key: ADGUARD_HOME_PASS securityContext: runAsUser: 0 runAsNonRoot: false allowPrivilegeEscalation: true livenessProbe: httpGet: path: /healthz port: 8888 readinessProbe: httpGet: path: /healthz port: 8888 service: port: 8888 domainFilters: - example.com extraArgs: - --webhook-provider-url=http://localhost:8888 serviceAccount: create: true name: external-dns rbac: create: true additionalPermissions: [] logLevel: info logFormat: text ``` ## Deploying an Nginx Service Create a service file called 'nginx.yaml' with the following contents: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: nginx name: nginx ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx annotations: external-dns.alpha.kubernetes.io/hostname: my-app.example.com spec: selector: app: nginx type: LoadBalancer ports: - protocol: TCP port: 80 targetPort: 80 ``` ExternalDNS uses `external-dns.alpha.kubernetes.io/hostname` annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. Create the deployment and service: ```console kubectl create -f nginx.yaml ``` Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service. Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the AdguardHome DNS records. ## Verifying AdguardHome DNS records Check your AdguardHome DNS records to see if the new record was created. Click on "Filters" and then "Custom filtering rules" This should show the external IP address of the service as the "Answer" for your domain. ## Cleanup Now that we have verified that ExternalDNS will automatically manage AdguardHome DNS records, we can delete the tutorial's example: ``` kubectl delete service -f nginx.yaml kubectl delete service -f externaldns.yaml ```