The TXT registry is the default registry. It stores DNS record metadata in TXT records, using the same provider.
Note:
- If you plan to manage apex domains with external-dns whilst using a txt registry, you should ensure when using
--txt-prefixthat you specify the record type substitution and that it ends in a period (.). The record should be created under the same domain as the apex record being managed, i.e.--txt-prefix=someprefix-%{record_type}.--txt-prefixand--txt-suffixcontribute to the 63-byte maximum record length. To avoid errors, use them only if absolutely required and keep them as short as possible.
The TXT registry supports single format for storing DNS record metadata:
- Creates a TXT record with record type information (e.g., 'a-' prefix for A records)
The TXT registry would try to guarantee a consistency in between providers and sources, if provider supports the behaviour.
If configured --txt-prefix="%{record_type}-abc-." for apex domain ex.com the expected result is
| Name | TYPE |
|---|---|
cname-abc-.ex.com. |
TXT |
ex.com. |
CNAME |
For the domain www.ex.com the expected result is
| Name | TYPE |
|---|---|
cname-abc-.www.ex.com. |
TXT |
www.ex.com. |
CNAME |
If configured --txt-suffix="-.%{record_type}" for apex domain ex.com, the expected result would be ex-.a.com, which fails to create a TXT record because it does not exist within the managed zone.
For the domain www.ex.com the expected result is
| Name | TYPE |
|---|---|
www-.cname.ex.com. |
TXT |
www.ex.com. |
CNAME |
AWS ALIAS records are stored in Route 53 as
A/AAAA records,
so their ownership TXT uses the matching a-/aaaa- prefix. For A ALIAS records this replaces the
legacy cname- prefix: the old cname- record is still recognized (ownership is preserved), the a-
record is created on the next reconciliation, and the obsolete cname- is left in place with a warning.
See #2903.
While deleting registry TXT records won't cause downtime, a well-thought-out migration and cleanup plan is crucial.
Occasionally, it may be necessary to remove outdated TXT records from your registry.
An example script for AWS can be found in scripts/aws-cleanup-legacy-txt-records.py with instructions on how to run it.
The script performs targeted deletion of TXT records that include ResourceRecords matching the heritage=external-dns,external-dns/owner=default or similar pattern.
In the event of unintended deletion of all TXT records managed by external-dns, external-dns will initiate a full DNS record regeneration, along withTXT and non-TXT records. Just be aware, this operation's duration is directly proportional to the DNS estate size."
To remove the obsolete cname- records left by the AWS A ALIAS migration, run the script with
--alias-cname-cleanup, which deletes a cname- record only when its a- counterpart exists.
The TXT registry supports two formats for storing DNS record metadata:
- Legacy format: Creates a TXT record without record type information
- New format: Creates a TXT record with record type information (e.g., 'a-' prefix for A records)
By default, the TXT registry creates records in both formats for backwards compatibility. You can configure it to use only the new format by using the --txt-new-format-only flag. This reduces the number of TXT records created, which can be helpful when working with provider-specific record limits.
Note: The following record types always use only the new format regardless of this setting:
- AAAA records
- Encrypted TXT records (when using
--txt-encrypt-enabled)
Example:
# Default behavior - creates both formats
external-dns --provider=aws --source=ingress --managed-record-types=A --managed-record-types=TXT
# Only create new format records (alongside other required flags)
external-dns --provider=aws --source=ingress --managed-record-types=A --managed-record-types=TXT --txt-new-format-onlyThe --txt-new-format-only flag should be used in addition to your existing external-dns configuration flags. It does not implicitly configure TXT record handling - you still need to specify --managed-record-types=TXT if you want external-dns to manage TXT records.
Note:
external-dnswill not automatically remove legacy format records when switching to new-format-only mode. You'll need to clean up the old records manually if desired.
When transitioning from dual-format to new-format-only records:
- Ensure all your
external-dnsinstances support the new format - Enable the
--txt-new-format-onlyflag on your external-dns instances Manually clean up any existing legacy format TXT records from your DNS provider
In order to avoid having the registry TXT records collide with TXT or CNAME records created from sources, you can configure a fixed prefix or suffix to be added to the first component of the domain of all registry TXT records.
The prefix or suffix may not be changed after initial deployment, lest the registry records be orphaned and the metadata be lost.
The prefix or suffix may contain the substring %{record_type}, which is replaced with
the record type of the DNS record for which it is storing metadata.
The prefix is specified using the --txt-prefix flag and the suffix is specified using
the --txt-suffix flag. The two flags are mutually exclusive.
The --txt-wildcard-replacement flag specifies a string to use to replace the "*" in
registry TXT records for wildcard domains. Without using this, registry TXT records for
wildcard domains will have invalid domain syntax and be rejected by most providers.
Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure. By encrypting TXT records, you can protect this information from unauthorized access.
Encryption is enabled by setting the --txt-encrypt-enabled. The 32-byte AES-256-GCM encryption
key must be specified in URL-safe base64 form (recommended) or be a plain text, using the --txt-encrypt-aes-key=<key> flag.
Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records.
Note: Encryption is best-effort. The stored value is produced by the Go standard library (AES-GCM over a gzip-compressed payload), and its exact bytes are not guaranteed to stay stable across Go toolchain or dependency upgrades. A future change could affect the ability to update or delete existing encrypted records on providers that match by value. Decryption of already-written records is not expected to be affected. Enable encryption with this in mind.
Rotating the encryption key is a manual, stop-the-world operation — external-dns only holds one key at a time and cannot decrypt records under an old key while encrypting new ones mid-flight.
Current behavior when a TXT ownership record cannot be decrypted (for example after the AES key has changed):
- AES-GCM authentication fails under the current key, so label parsing returns an error and external-dns cannot recover the owner metadata.
- It doesn't error out or crash. It quietly treats the record as unowned (no owner label).
- Because it no longer recognizes the record as its own, it refuses to update or delete it going forward.
- The record already exists, so external-dns:
- won't try to re-create it or take ownership of it
- won't apply any future change to its desired value either, since it doesn't own it
- lets it silently drift, unmanaged
- Anything created afterward works fine, since it's encrypted fresh under whatever key is currently configured.
This is why key rotation specifically needs the migration below rather than just swapping the flag.
- Stop external-dns (scale the deployment to 0 or pause it) before starting the migration below, so nothing else is writing to the zone concurrently.
- For every existing encrypted TXT record in the zone — there's no ready-made tool for this, write a
small CLI using your DNS provider's SDK to list them:
- Decrypt with the old key:
endpoint.NewLabelsFromString(value, oldKey). - Delete the
txt-encryption-nonceentry from the returned map so a fresh nonce is generated on re-encrypt. - Re-encrypt with the new key:
labels.Serialize(true, true, newKey)(withQuotes=true,txtEncryptEnabled=true). - Write the new value back to the provider via its API.
- Decrypt with the old key:
- Once all records are migrated, update
--txt-encrypt-aes-key(or the backing secret) to the new key. - Run external-dns once with
--dry-run(pointed at the new key) and check the logs before applying for real:- Confirm no unexpected creates/updates/deletes (DNS record drift).
- Confirm no decryption errors are logged.
- Restart external-dns without
--dry-run. On the next reconcile it will decrypt every record successfully under the new key.
Note: There is no built-in
external-dnscommand or generic CLI provided by this repository for this — you'll need to build one against your provider's SDK. If you build something generic enough to be reusable across providers, or learn something worth sharing along the way, please consider contributing it back or updating this guide.
-
Run two (or more) external-dns instances instead of encrypting everything. One instance without
--txt-encrypt-enabledfor records that don't carry sensitive metadata; a second instance with--txt-encrypt-enabled+--txt-encrypt-aes-key, scoped to only the resources that actually need it. -
Don't encrypt by default. Encryption isn't free — it adds CPU overhead per reconcile and carries the risk noted above. Private / internal-only records whose metadata (ingress name, namespace, resource path) isn't sensitive generally don't need it.
-
Scope the encrypting instance with
--label-filterso only resources explicitly opted in get encrypted, rather than blanket-encrypting a whole cluster. Pick one label convention and apply it consistently:Label key Label value --label-filterexternal-dns/txt-encrypttrue--label-filter=external-dns/txt-encrypt=trueexternal-dns.io/sensitivetrue--label-filter=external-dns.io/sensitive=truesecurity-tierconfidential--label-filter=security-tier=confidentialPoint the non-encrypting instance at the inverse selector (e.g.
--label-filter='external-dns/txt-encrypt notin (true)') so the two instances don't both pick up the same resource. Keep the label convention stable: a resource that moves between the two filters forces an ownership handoff and a rewrite of its TXT record (encrypted to plain, or the reverse). -
Have a disaster-recovery plan for TXT records, regardless of encryption. TXT records are the only record of external-dns's ownership metadata — losing or corrupting them (accidental deletion, botched migration, provider outage) can cause external-dns to lose track of what it owns, leading to orphaned records or unintended recreation/deletion. Back up zone contents (including TXT records) and document a recovery procedure before you need it; this applies equally whether encryption is enabled or not.
Python
python -c 'import os,base64; print(base64.standard_b64encode(os.urandom(32)).decode())'Bash
dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64; echoOpenSSL
openssl rand -base64 32PowerShell
# Add System.Web assembly to session, just in case
Add-Type -AssemblyName System.Web
[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([System.Web.Security.Membership]::GeneratePassword(32,4)))Terraform
resource "random_password" "txt_key" {
length = 32
}In some cases you might need to edit registry TXT records. The following example Go code encrypts and decrypts such records.
package main
import (
b64 "encoding/base64"
"fmt"
"sigs.k8s.io/external-dns/endpoint"
)
func main() {
keys := []string{
"ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=", // safe base64 url encoded 44 bytes and 32 when decoded
"01234567890123456789012345678901", // plain txt 32 bytes
"passphrasewhichneedstobe32bytes!", // plain txt 32 bytes
}
for _, k := range keys {
key := []byte(k)
if len(key) != 32 {
// if key is not a plain txt let's decode
var err error
if key, err = b64.StdEncoding.DecodeString(string(key)); err != nil || len(key) != 32 {
fmt.Errorf("the AES Encryption key must have a length of 32 byte")
}
}
encrypted, _ := endpoint.EncryptText(
"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example",
key,
nil,
)
decrypted, _, err := endpoint.DecryptText(encrypted, key)
if err != nil {
fmt.Println("Error decrypting:", err, "for key:", k)
}
fmt.Println(decrypted)
}
}The TXT registry can optionally cache DNS records read from the provider. This can mitigate rate limits imposed by the provider.
Caching is enabled by specifying a cache duration with the --txt-cache-interval flag.
Automating DNS migrations with third-party tools can be risky. DNS is often business-critical, and without deep understanding of the environment, 3rd party automation tools can do more harm than good.
The owner ID of the TXT records managed by external-dns instance can be updated.
When --migrate-from-txt-owner is set, it will enable the migration checks
in the run loop using --txt-owner-id=new-owner-id and the value you defined for this flag.
If you want to test the outputs of a migration beforehand, you can use the --dry-run flag
along with --migrate-from-txt-owner.
Example, if you had a standard deployment like so:
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.22.0
imagePullPolicy: Always
args:
- "--txt-prefix=%{record_type}-"
- "--txt-cache-interval=2m"
- "--log-level=debug"
- "--log-format=text"
- "--txt-owner-id=old-owner"
- "--policy=sync"
- "--provider=some-provider"
- "--registry=txt"
- "--interval=1m"
- "--source=ingress"You can update your deployment to migrate like so :
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
imagePullPolicy: Always
image: registry.k8s.io/external-dns/external-dns:v0.22.0
args:
- "--txt-prefix=%{record_type}-"
- "--txt-cache-interval=2m"
- "--log-level=debug"
- "--log-format=text"
- "--txt-owner-id=new-owner"
- "--migrate-from-txt-owner=old-owner"
- "--policy=sync"
- "--provider=some-provider"
- "--registry=txt"
- "--interval=1m"
- "--source=ingress"If you didn't set the owner ID, the value set by external-dns is default. You can set the
--migrate-from-txt-owner flag to default to migrate the associated records.
Warning: The
--migrate-from-txt-ownerflag combined withpolicy=synccan be unsafe in shared hosted zones when multiple clusters previously used the same TXT owner value (for exampledefault).
In a shared hosted zone, if one cluster runs ExternalDNS with policy=sync and --migrate-from-txt-owner=default, it may attempt to delete DNS records that belong to other clusters which still use owner=default.
To avoid this, do not share the same TXT owner value across clusters in any zone where policy=sync or migration flags will be used.
For multi-cluster setups sharing a hosted zone:
- Assign a unique
--txt-owner-idto each cluster (for examplecluster1,cluster2) and document this convention clearly in your platform configuration. - Avoid using a common owner such as
defaultacross clusters in a shared zone if any cluster will run withpolicy=syncor use--migrate-from-txt-owner.
When migrating from a shared owner (such as default) in a shared hosted zone:
- While still using
policy=upsert-only(or equivalent), roll out cluster-specific--txt-owner-idvalues and ensure new records are created with the cluster’s own owner ID. - Avoid
--migrate-from-txt-owner=<old-owner>unless you can guarantee that only a single cluster has records with<old-owner>in that hosted zone, or perform the migration in an isolated zone where only that cluster writes records.
The following pattern is not recommended and may cause record deletion for other clusters:
- Multiple clusters share a Route53 hosted zone and all existing records use
owner=default. - Only one cluster is upgraded to use
policy=sync,--txt-owner-id=<cluster-name>, and--migrate-from-txt-owner=default, while other clusters still useowner=default.
In this situation, the upgraded cluster can treat other clusters’ records as orphans and schedule them for deletion during synchronization. Prefer per-cluster zones, manual TXT record adjustment, or fully coordinated migration of all clusters if the migration flag must be used.