Skip to content

RFC2136 provider

This tutorial describes how to use the RFC2136 with either BIND or Windows DNS.

Deploying BIND9 on Kubernetes with Kind

This section walks through deploying BIND9 and ExternalDNS inside a local Kubernetes cluster using Kind. It provides a self-contained lab environment for testing the RFC2136 provider, including forward and reverse (PTR) DNS zones with TSIG authentication.

TL;DR

After completing this lab, you will have a local Kubernetes cluster running BIND9 and ExternalDNS with:

  • A forward zone (example.local) for A records
  • A reverse zone (49.168.192.in-addr.arpa) for PTR records
  • TSIG-authenticated dynamic DNS updates
  • ExternalDNS automatically creating and managing DNS records

Notes

  • BIND9 runs inside the cluster for demonstration purposes.
  • For production deployments, use an external BIND server with proper security hardening.
  • The zones and TSIG key are preconfigured — adapt them to your environment.

Prerequisites

Before you start, ensure you have:

1. Create the Kind cluster

kind create cluster --config=docs/snippets/tutorials/rfc2136/kind.yaml

Creating cluster "rfc2136-bind9" ...
  Ensuring node image (kindest/node:v1.35.1) 🖼
  Preparing nodes 📦 📦
  Writing configuration 📜
  Starting control-plane 🕹️
  Installing CNI 🔌
  Installing StorageClass 💾
  Joining worker nodes 🚜
Set kubectl context to "kind-rfc2136-bind9"

The Kind configuration exposes port 30053 on the host as 5354, allowing DNS queries from your machine against BIND9 running inside the cluster.

2. Deploy BIND9

The BIND9 manifest deploys a ConfigMap with named.conf, forward and reverse zone files, a Deployment, and Services (ClusterIP + NodePort).

The configuration includes:

  • Forward zone (example.local): accepts dynamic updates authenticated with the externaldns-key TSIG key
  • Reverse zone (49.168.192.in-addr.arpa): accepts dynamic updates and zone transfers with the same TSIG key
kubectl apply -f docs/snippets/tutorials/rfc2136/bind9.yaml

kubectl rollout status deployment/bind9 -n bind9

❯❯ deployment "bind9" successfully rolled out

Verify BIND9 is running:

kubectl get pods -n bind9

❯❯ NAME                     READY   STATUS    RESTARTS   AGE
❯❯ bind9-xxxxxxxxx-xxxxx    1/1     Running   0          30s

Check BIND9 logs for errors:

kubectl logs -n bind9 -l app=bind9 --tail=20

3. Deploy ExternalDNS with Helm

Add the ExternalDNS Helm repository:

helm repo add --force-update external-dns https://kubernetes-sigs.github.io/external-dns/

Install ExternalDNS with the RFC2136 provider configuration:

helm upgrade --install external-dns external-dns/external-dns \
  -f docs/snippets/tutorials/rfc2136/values-extdns-rfc2136.yaml \
  -n default

❯❯ Release "external-dns" does not exist. Installing it now.

Validate pod status and view logs:

kubectl get pods -l app.kubernetes.io/name=external-dns

kubectl logs deploy/external-dns --tail=50

Or run ExternalDNS from source on the host (requires the NodePort to be accessible):

go run main.go \
    --provider=rfc2136 \
    --rfc2136-host=127.0.0.1 \
    --rfc2136-port=5354 \
    --rfc2136-zone=example.local \
    --rfc2136-zone=49.168.192.in-addr.arpa \
    --rfc2136-tsig-keyname=externaldns-key \
    --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= \
    --rfc2136-tsig-secret-alg=hmac-sha256 \
    --rfc2136-tsig-axfr \
    --create-ptr \
    --source=crd \
    --source=service \
    --domain-filter=example.local \
    --domain-filter=49.168.192.in-addr.arpa \
    --managed-record-types=A \
    --managed-record-types=AAAA \
    --managed-record-types=CNAME \
    --managed-record-types=PTR \
    --policy=sync \
    --log-level=debug

4. Create test DNS records

Apply the test fixtures which include DNSEndpoint CRDs and a LoadBalancer Service:

kubectl apply -f docs/snippets/tutorials/rfc2136/fixtures.yaml

If using Service sources, patch the LoadBalancer to simulate an external IP assignment:

kubectl patch svc nginx-rfc2136 --type=merge \
  -p '{"status":{"loadBalancer":{"ingress":[{"ip":"192.168.49.50"}]}}}' \
  --subresource=status

❯❯ service/nginx-rfc2136 patched

Wait for ExternalDNS to sync (check the logs for update activity):

kubectl logs deploy/external-dns --tail=30 -f

5. Verify DNS records

Query the forward zone for A records:

dig @127.0.0.1 -p 5354 +tcp app.example.local A +short
❯❯ 192.168.49.10

dig @127.0.0.1 -p 5354 +tcp api.example.local A +short
❯❯ 192.168.49.20

dig @127.0.0.1 -p 5354 +tcp svc.example.local A +short
❯❯ 192.168.49.50

Query the reverse zone for PTR records:

dig @127.0.0.1 -p 5354 +tcp -x 192.168.49.10 +short
❯❯ app.example.local.

dig @127.0.0.1 -p 5354 +tcp -x 192.168.49.20 +short
❯❯ api.example.local.

List all records in the reverse zone via AXFR (requires the TSIG key since zone transfers are restricted):

dig @127.0.0.1 -p 5354 +tcp 49.168.192.in-addr.arpa. AXFR \
  -y hmac-sha256:externaldns-key:96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= \
  +noall +answer

Alternatively, query from inside the cluster using a debug pod:

kubectl run --rm -it dnsutils --image=infoblox/dnstools --restart=Never

dig +short @bind9.bind9.svc.cluster.local app.example.local
❯❯ 192.168.49.10

dig +short @bind9.bind9.svc.cluster.local -x 192.168.49.10
❯❯ app.example.local.

6. Cleanup

kind delete cluster --name rfc2136-bind9

Using with BIND

To use external-dns with BIND: generate/procure a key, configure DNS and add a
deployment of external-dns.

Server credentials

  • RFC2136 was developed for and tested with BIND DNS server.
    This documentation assumes that you already have a configured and working server. If you don’t,
    please check BIND documents or tutorials.
  • If your DNS is provided for you, ask for a TSIG key authorized to update and
    transfer the zone you wish to update. The key will look something like below.
    Skip the next steps wrt BIND setup.
key "externaldns-key" {
 algorithm hmac-sha256;
 secret "96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=";
};
  • If you are your own DNS administrator create a TSIG key. Use
    tsig-keygen -a hmac-sha256 externaldns or on older distributions
    dnssec-keygen -a HMAC-SHA256 -b 256 -n HOST externaldns. You will end up with
    a key printed to standard out like above (or in the case of dnssec-keygen in a
    file called Kexternaldns......key).

BIND Configuration

If you do not administer your own DNS, skip to RFC provider configuration

  • Edit your named.conf file (or appropriate included file) and add/change the
    following.

    • Make sure You are listening on the right interfaces. At least whatever
      interface external-dns will be communicating over and the interface that
      faces the internet.
    • Add the key that you generated/was given to you above. Copy paste the four
      lines that you got (not the same as the example key) into your file.
    • Make sure zone transfer is enabled for the key, this enables listing all
      records
    • Create a zone for kubernetes. If you already have a zone, skip to the next
      step. (I put the zone in it’s own subdirectory because named,
      which shouldn’t be running as root, needs to create a journal file and the
      default zone directory isn’t writeable by named).
    zone "k8s.example.org" {
        type master;
        file "/etc/bind/pri/k8s/k8s.zone";
    };
    
    • Add your key to both transfer and update. For instance with our previous
      zone.
    zone "k8s.example.org" {
        type master;
        file "/etc/bind/pri/k8s/k8s.zone";
        allow-transfer {
            key "externaldns-key";
        };
        update-policy {
            grant externaldns-key zonesub ANY;
        };
    };
    
    • Create a zone file (k8s.zone):
    $TTL 60 ; 1 minute
    k8s.example.org         IN SOA  k8s.example.org. root.k8s.example.org. (
                                    16         ; serial
                                    60         ; refresh (1 minute)
                                    60         ; retry (1 minute)
                                    60         ; expire (1 minute)
                                    60         ; minimum (1 minute)
                                    )
                            NS      ns.k8s.example.org.
    ns                      A       123.456.789.012
    
    • Reload (or restart) named

AXFR and the sync policy

When using the sync policy, ExternalDNS requires AXFR (zone transfer) to be
explicitly enabled via the --rfc2136-tsig-axfr flag. This is necessary for
ExternalDNS to list all existing DNS records and determine which ones should be
lifecycled.

Without --rfc2136-tsig-axfr, ExternalDNS cannot list records and will act as
if the policy was set to upsert-only. No warning will be provided.

Using external-dns

To use external-dns add an ingress or a LoadBalancer service with a host that
is part of the domain-filter. For example both of the following would produce
A records.

apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    external-dns.alpha.kubernetes.io/hostname: svc.example.org
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    name: my-ingress
spec:
    rules:
    - host: ingress.example.org
      http:
          paths:
          - path: /
            backend:
                serviceName: my-service
                servicePort: 8000

Custom TTL

The default DNS record TTL (Time-To-Live) is 0 seconds. You can customize this value by setting the annotation external-dns.alpha.kubernetes.io/ttl. e.g., modify the service manifest YAML file above:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
    external-dns.alpha.kubernetes.io/ttl: 60
spec:
    ...

This will set the DNS record’s TTL to 60 seconds.

A default TTL for all records can be set using the the flag with a time in seconds, minutes or hours, such as --rfc2136-min-ttl=60s

There are other annotation that can affect the generation of DNS records, but these are beyond the scope of this
tutorial and are covered in the main documentation.

Generate reverse DNS records

Note: The --rfc2136-create-ptr flag is removed. Use the generic --create-ptr flag instead.

ExternalDNS can automatically create PTR records for your A/AAAA records. This is a generic feature
that works with any provider, not just RFC2136. For RFC2136 you also need to add the reverse zone to
--rfc2136-zone:

--create-ptr --domain-filter=157.168.192.in-addr.arpa --rfc2136-zone=157.168.192.in-addr.arpa

See Automatic PTR (Reverse DNS) Records for full documentation
including annotation overrides and behaviour details.

Test with external-dns installed on local machine (optional)

You may install external-dns and test on a local machine by running:

external-dns --txt-owner-id k8s --provider rfc2136 \
  --rfc2136-host=192.168.0.1 --rfc2136-port=53 \
  --rfc2136-zone=k8s.example.org \
  --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8= \
  --rfc2136-tsig-secret-alg=hmac-sha256 \
  --rfc2136-tsig-keyname=externaldns-key \
  --rfc2136-tsig-axfr \
  --source ingress --once \
  --domain-filter=k8s.example.org --dry-run
  • host should be the IP of your master DNS server.
  • tsig-secret should be changed to match your secret.
  • tsig-keyname needs to match the keyname you used (if you changed it).
  • domain-filter can be used as shown to filter the domains you wish to update.

RFC2136 provider configuration

In order to use external-dns with your cluster you need to add a deployment
with access to your ingress and service resources. The following are two
example manifests with and without RBAC respectively.

  • With RBAC:
apiVersion: v1
kind: Namespace
metadata:
  name: external-dns
  labels:
    name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: external-dns
  namespace: external-dns
rules:
- apiGroups:
  - ""
  resources:
  - services
  - pods
  - nodes
  verbs:
  - get
  - watch
  - list
- apiGroups:
  - discovery.k8s.io
  resources:
  - endpointslices
  verbs:
  - get
  - watch
  - list
- apiGroups:
  - extensions
  - networking.k8s.io
  resources:
  - ingresses
  verbs:
  - get
  - list
  - watch
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-dns
  namespace: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: external-dns-viewer
  namespace: external-dns
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: external-dns
subjects:
- kind: ServiceAccount
  name: external-dns
  namespace: external-dns
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: external-dns
spec:
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      serviceAccountName: external-dns
      containers:
      - name: external-dns
        image: registry.k8s.io/external-dns/external-dns:v0.21.0
        args:
        - --registry=txt
        - --txt-prefix=external-dns-
        - --txt-owner-id=k8s
        - --provider=rfc2136
        - --rfc2136-host=192.168.0.1
        - --rfc2136-port=53
        - --rfc2136-zone=k8s.example.org
        - --rfc2136-zone=k8s.your-zone.org
        - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=
        - --rfc2136-tsig-secret-alg=hmac-sha256
        - --rfc2136-tsig-keyname=externaldns-key
        - --rfc2136-tsig-axfr
        - --source=ingress
        - --domain-filter=k8s.example.org
  • Without RBAC:
apiVersion: v1
kind: Namespace
metadata:
  name: external-dns
  labels:
    name: external-dns
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: external-dns
  namespace: external-dns
spec:
  selector:
    matchLabels:
      app: external-dns
  template:
    metadata:
      labels:
        app: external-dns
    spec:
      containers:
      - name: external-dns
        image: registry.k8s.io/external-dns/external-dns:v0.21.0
        args:
        - --registry=txt
        - --txt-prefix=external-dns-
        - --txt-owner-id=k8s
        - --provider=rfc2136
        - --rfc2136-host=192.168.0.1
        - --rfc2136-port=53
        - --rfc2136-zone=k8s.example.org
        - --rfc2136-zone=k8s.your-zone.org
        - --rfc2136-tsig-secret=96Ah/a2g0/nLeFGK+d/0tzQcccf9hCEIy34PoXX2Qg8=
        - --rfc2136-tsig-secret-alg=hmac-sha256
        - --rfc2136-tsig-keyname=externaldns-key
        - --rfc2136-tsig-axfr
        - --source=ingress
        - --domain-filter=k8s.example.org

Microsoft DNS

While external-dns was not developed or tested against Microsoft DNS, it can be configured to work against it. YMMV.

Secure Updates Using RFC3645 (GSS-TSIG)

DNS-side configuration

  1. Create a DNS zone
  2. Enable secure dynamic updates for the zone
  3. Enable Zone Transfers to all servers and/or other domains
  4. Create a user with permissions to create/update/delete records in that zone

If you see any error messages which indicate that external-dns was somehow not able to fetch
existing DNS records from your DNS server, this could mean that you forgot about step 3.

Kerberos Configuration

DNS with secure updates relies upon a valid Kerberos configuration running within the external-dns container.
At this time, you will need to create a ConfigMap for the external-dns container to use and mount it in your deployment.
Below is an example of a working Kerberos configuration inside a ConfigMap definition. This may be different depending on many factors in your environment:

apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: krb5.conf
data:
  krb5.conf: |
    [logging]
    default = FILE:/var/log/krb5libs.log
    kdc = FILE:/var/log/krb5kdc.log
    admin_server = FILE:/var/log/kadmind.log

    [libdefaults]
    dns_lookup_realm = false
    ticket_lifetime = 24h
    renew_lifetime = 7d
    forwardable = true
    rdns = false
    pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt
    default_ccache_name = KEYRING:persistent:%{uid}

    default_realm = YOUR-REALM.COM

    [realms]
    YOUR-REALM.COM = {
      kdc = dc1.yourdomain.com
      admin_server = dc1.yourdomain.com
    }

    [domain_realm]
    yourdomain.com = YOUR-REALM.COM
    .yourdomain.com = YOUR-REALM.COM

In most cases, the realm name will probably be the same as the domain name, so you can simply replace YOUR-REALM.COM with something like YOURDOMAIN.COM.

Once the ConfigMap is created, the container external-dns container needs to be told to mount that ConfigMap as a volume at the default Kerberos configuration location. The pod spec should include a similar configuration to the following:

...
    volumeMounts:
    - mountPath: /etc/krb5.conf
      name: kerberos-config-volume
      subPath: krb5.conf
...
  volumes:
  - configMap:
      defaultMode: 420
      name: krb5.conf
    name: kerberos-config-volume
...
external-dns configuration

You’ll want to configure external-dns similarly to the following:

...
        - --provider=rfc2136
        - --rfc2136-gss-tsig
        - --rfc2136-host=dns-host.yourdomain.com
        - --rfc2136-port=53
        - --rfc2136-zone=your-zone.com
        - --rfc2136-zone=your-secondary-zone.com
        - --rfc2136-kerberos-username=your-domain-account
        - --rfc2136-kerberos-password=your-domain-password
        - --rfc2136-kerberos-realm=your-domain.com
        - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
...

As noted above, the --rfc2136-kerberos-realm flag is completely optional and won’t be necessary in many cases.
Most likely, you will only need it if you see errors similar to this: KRB Error: (68) KDC_ERR_WRONG_REALM Reserved for future use.

The flag --rfc2136-host can be set to the host’s domain name or IP address.
However, it also determines the name of the Kerberos principal which is used during authentication.
This means that Active Directory might only work if this is set to a specific domain name, possibly leading to errors like this:
KDC_ERR_S_PRINCIPAL_UNKNOWN Server not found in Kerberos database.
To fix this, try setting --rfc2136-host to the “actual” hostname of your DNS server.

Insecure Updates

DNS-side configuration

  1. Create a DNS zone
  2. Enable insecure dynamic updates for the zone
  3. Enable Zone Transfers to all servers and/or other domains

external-dns configuration

You’ll want to configure external-dns similarly to the following:

...
        - --provider=rfc2136
        - --rfc2136-host=192.168.0.1
        - --rfc2136-port=53
        - --rfc2136-zone=k8s.example.org
        - --rfc2136-zone=k8s.your-zone.org
        - --rfc2136-insecure
        - --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
...

DNS Over TLS (RFCs 7858 and 9103)

If your DNS server does zone transfers over TLS, you can instruct external-dns to connect over TLS with the following flags:

  • --rfc2136-use-tls Will enable TLS for both zone transfers and for updates.
  • --tls-ca=<cert-file> Is the path to a file containing certificate(s) that can be used to verify the DNS server
  • --tls-client-cert=<client-cert-file> and
  • --tls-client-cert-key=<client-key-file> Set the client certificate and key for mutual verification
  • --rfc2136-skip-tls-verify Disables verification of the certificate supplied by the DNS server.

It is currently not supported to do only zone transfers over TLS, but not the updates. They are enabled and disabled together.

Configuring RFC2136 Provider with Multiple Hosts and Load Balancing

This section describes how to configure the RFC2136 provider in ExternalDNS to support multiple DNS servers and load balancing options.

Enhancements Overview

The RFC2136 provider now supports multiple DNS hosts and introduces load balancing options to distribute DNS update requests evenly across available DNS servers. This helps prevent a single server from becoming a bottleneck in environments with multiple DNS servers.

Configuration Steps

  1. Allow Multiple Hosts for --rfc2136-host
    - Modify the --rfc2136-host command-line option to accept multiple hosts.
    - Example: --rfc2136-host="dns-host-1.yourdomain.com" --rfc2136-host="dns-host-2.yourdomain.com"

  2. Introduce Load Balancing Options
    - Add a new command-line option --rfc2136-load-balancing-strategy to specify the load balancing strategy.
    - Supported options:
    - round-robin: Distributes DNS updates evenly across all specified hosts in a round-robin manner.
    - random: Randomly selects a host for each DNS update.
    - disabled (default): Uses the first host in the list as the primary, only moving to the next host if a failure occurs.

Example Configuration

external-dns \
  --provider=rfc2136 \
  --rfc2136-host="dns-host-1.yourdomain.com" \
  --rfc2136-host="dns-host-2.yourdomain.com" \
  --rfc2136-host="dns-host-3.yourdomain.com" \
  --rfc2136-load-balancing-strategy="round-robin" \
  --rfc2136-port=53 \
  --rfc2136-zone=example.com \
  --rfc2136-tsig-secret-alg=hmac-sha256 \
  --rfc2136-tsig-keyname=example-key \
  --rfc2136-tsig-secret=example-secret \
  --rfc2136-insecure

Helm

extraArgs:
  - --rfc2136-host="dns-host-1.yourdomain.com"
  - --rfc2136-port=53
  - --rfc2136-zone=example.com
  - --rfc2136-tsig-secret-alg=hmac-sha256
  - --rfc2136-tsig-axfr

env:
  - name: "EXTERNAL_DNS_RFC2136_TSIG_SECRET"
    valueFrom:
      secretKeyRef:
        name: rfc2136-keys
        key: rfc2136-tsig-secret
  - name: "EXTERNAL_DNS_RFC2136_TSIG_KEYNAME"
    valueFrom:
      secretKeyRef:
        name: rfc2136-keys
        key: rfc2136-tsig-keyname

Secret creation

kubectl create secret generic rfc2136-keys --from-literal=rfc2136-tsig-secret='xxx' --from-literal=rfc2136-tsig-keyname='k8s-external-dns-key' -n external-dns

Benefits

  • Distributes the load of DNS updates across multiple data centers, preventing any single DC from becoming a bottleneck.
  • Provides flexibility to choose different load balancing strategies based on the environment and requirements.
  • Improves the resilience and reliability of DNS updates by introducing a retry mechanism with a list of hosts.