Kubernetes DNS and TLS operations with DNScale
Quick answer
ExternalDNS maintains application DNS and its ownership TXT records; cert-manager maintains certificate challenge TXT records and TLS Secrets. Give each controller separate credentials and record ownership, test one application with Let's Encrypt staging, and verify DNS and the served certificate independently. Monitor renewals and reconciliation failures, preserve ownership during upgrades, and pause the responsible writer before repairing records.
On this page
What you'll learn
- Define separate DNS ownership and credentials for both controllers
- Connect an Ingress to application DNS and a managed certificate
- Verify certificate renewal, DNS reconciliation, and the served TLS certificate
- Rotate credentials, upgrade controllers, and recover from failed changes
ExternalDNS and cert-manager handle different parts of an application's public endpoint. ExternalDNS follows the address published by a Service or Ingress. cert-manager obtains and renews the certificate the ingress controller serves. DNScale supplies the authoritative DNS records used by both workflows.
This guide covers their operation together after installation. Use ExternalDNS on Kubernetes and cert-manager DNS-01 with DNScale for the Helm commands, token creation, and issuer setup.
Establish ownership before enabling writes
Use a dedicated delegated zone, such as k8s.example.org, that already exists
in DNScale. Assign each name and record type to one writer:
| Resource | Owner | Example |
|---|---|---|
| Application A, AAAA, or CNAME | ExternalDNS | web.k8s.example.org |
| Application ownership TXT | ExternalDNS | edns-a.web.k8s.example.org for an A record |
| ACME challenge TXT values | cert-manager through its DNScale webhook | _acme-challenge.web.k8s.example.org |
| TLS Secret | cert-manager | applications/web-production-tls |
| Load-balancer address and served TLS configuration | Your ingress controller | Ingress applications/web |
| Zone delegation, CAA, and other zone policy | Your DNS operations process | NS and CAA for k8s.example.org |
Keep ExternalDNS's txtOwnerId and txtPrefix: "edns-%{record_type}." stable.
Its ownership applies to a complete name/type record set, including every IP
target. Separate owner IDs do not make it safe for two controllers to manage
the same record set. Run one active ExternalDNS controller per ownership scope.
Use different DNScale tokens for the controllers, each with zones:read,
records:read, and records:write. Scope the cert-manager token to the exact
challenge owners, such as _acme-challenge.web relative to this zone. Scope
ExternalDNS to its application zone; if using exact DNS-name restrictions,
include its application names and ownership TXT names. A name restriction is
not a subdomain wildcard or a record-type restriction.
With the installation guides' defaults, ExternalDNS uses Secret
external-dns/dnscale-api-token, key token. The cert-manager ClusterIssuer
uses cert-manager/dnscale-api-token, key api-token. These are distinct
Secrets despite their shared name. For a namespaced Issuer, put its credential
Secret in that Issuer's namespace and grant the webhook access through
secretAccess.
Record the installed versions
The released providers were verified against this baseline:
| Component | Version |
|---|---|
| Kubernetes | 1.35.0 |
| ExternalDNS controller / official chart | v0.23.0 / 1.23.0 |
| DNScale ExternalDNS webhook image | v1.0.0 |
| cert-manager | 1.19.3 |
| DNScale cert-manager webhook image / chart | v1.0.0 / 1.0.0 |
These are tested combinations, not a requirement to keep older components indefinitely. Record your own versions and validate upgrades in a test scope. Keep the reviewed Helm values and image digests alongside your deployment configuration. The ExternalDNS release and cert-manager webhook release provide the versioned artifacts.
Connect one application to DNS and TLS
Start with a disposable hostname. The example below assumes:
- The two integrations are installed, and
letsencrypt-staging-dnscaleis a readyClusterIssuerfrom the cert-manager guide. - A working HTTP Service named
webexists on port 80 inapplications. - Your ingress controller publishes a reachable address in Ingress status.
- ExternalDNS watches
applicationswithlabelFilter: external-dns=enabledand starts with bothDNSCALE_DRY_RUN=trueandextraArgs.dry-run: true.
Replace the hostname and your-ingress-class with your test domain and actual
IngressClass. Save this as web-staging.yaml:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: web-staging
namespace: applications
spec:
secretName: web-staging-tls
issuerRef:
name: letsencrypt-staging-dnscale
kind: ClusterIssuer
dnsNames:
- web.k8s.example.org
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: applications
labels:
external-dns: enabled
annotations:
external-dns.kubernetes.io/hostname: web.k8s.example.org
external-dns.kubernetes.io/ttl: "300"
spec:
ingressClassName: your-ingress-class
tls:
- hosts:
- web.k8s.example.org
secretName: web-staging-tls
rules:
- host: web.k8s.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80This example explicitly manages the Certificate, so it does not also use a
cert-manager issuer annotation on the Ingress. The referenced TLS Secret must
be in the Ingress namespace. See Kubernetes' Ingress TLS documentation
for the host and Secret relationship.
kubectl apply -f web-staging.yaml
kubectl -n applications get ingress web
kubectl -n applications describe certificate web-staging
kubectl -n external-dns logs deployment/external-dns -c external-dns --tail=100
kubectl -n external-dns logs deployment/external-dns -c webhook --tail=100Review the proposed DNS names and targets. Then change both dry-run settings to
false in your complete ExternalDNS values file and apply the Helm upgrade
from its installation guide, keeping policy: upsert-only. The DNScale
sidecar's dry-run setting is essential: the tested controller does not forward
its own dry-run flag through the webhook protocol. ExternalDNS dry-run does
not prevent cert-manager from writing its challenge TXT records.
Select only the Ingress for this hostname; do not also publish the same name from its Service. Give other clusters and Terraform separate record ownership.
Verify DNS, issuance, and the served endpoint
Check authoritative DNS before resolver caches. Find the zone's assigned
nameservers, then replace ns1.example.net below with one of them:
dig +short NS k8s.example.org
dig @ns1.example.net web.k8s.example.org A +short
dig @ns1.example.net web.k8s.example.org AAAA +short
dig @ns1.example.net web.k8s.example.org CNAME +short
dig @1.1.1.1 web.k8s.example.org A +shortExpect the records appropriate to the Ingress address: A for IPv4, AAAA for
IPv6, or CNAME for a hostname. Check the corresponding ownership TXT, such as
edns-a.web.k8s.example.org, edns-aaaa.web.k8s.example.org, or
edns-cname.web.k8s.example.org. An empty response for an unused record type is
normal. Allow the configured two-minute reconciliation interval and resolver
cache expiry before treating a stale answer as a failure.
For initial certificate issuance:
kubectl -n applications wait certificate/web-staging \
--for=condition=Ready --timeout=300s
kubectl -n applications get certificaterequests,orders,challenges
kubectl -n applications get secret web-staging-tlsStaging certificates are not browser-trusted. After staging succeeds, create a
separate Certificate named web-production, using Secret
web-production-tls and ClusterIssuer letsencrypt-production-dnscale from the
setup guide. Keep the same DNS name. Wait for that Certificate to become Ready,
then change the Ingress manifest's tls[].secretName to web-production-tls
and apply it. Keep staging and production account keys separate.
Verify the certificate actually served after the ingress controller reloads:
curl --fail --show-error --silent https://web.k8s.example.org/ --output /dev/null
openssl s_client -connect web.k8s.example.org:443 \
-servername web.k8s.example.org </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -datesUse your application's health path if / does not return a successful status.
The curl check verifies normal TLS trust; the OpenSSL output helps identify the
served certificate. A Ready Certificate alone does not prove that clients
reach the intended load balancer or receive that certificate.
Monitor reconciliation and renewal
Monitor three outcomes independently: authoritative DNS matches the selected Ingress, certificate renewal finishes before expiry, and the public HTTPS endpoint remains healthy.
| Signal | Operational response |
|---|---|
| Repeated ExternalDNS or DNScale API failures | Inspect both containers' logs, token scope, ownership conflicts, and API throttling |
| Ingress address absent or unexpected | Check the ingress controller and load balancer before editing DNS |
| Certificate renewal overdue or expiry approaching | Inspect Certificate, CertificateRequest, Order, and Challenge events |
| Certificate Ready but HTTPS wrong | Check hostname resolution, ingress class, TLS Secret reference, and controller reload |
| Metrics scrape missing | Repair monitoring; missing samples do not mean zero errors |
The ExternalDNS sidecar exposes /metrics on its health port, 8080. Useful
counters include dnscale_webhook_failures_total, dnscale_api_failures_total,
and dnscale_api_throttles_total; alert on sustained increases rather than
their lifetime totals. A healthy Pod or /readyz response does not prove API
access or successful reconciliation. Keep the mutation listener on loopback.
Scrape cert-manager using its Prometheus integration. For example, this expression selects the example production certificate when less than 14 days remain:
(certmanager_certificate_expiration_timestamp_seconds{
namespace="applications", name="web-production"
} - time()) / 86400 < 14Fourteen days is an example threshold: choose warning and urgent thresholds that fit the actual certificate lifetime and your response time. Also monitor Certificate readiness and the external HTTPS endpoint. The metric's labels are defined in the tested cert-manager metrics source.
cert-manager reports the scheduled renewal in status.renewalTime; inspect
the issued certificate's actual notAfter instead of assuming a fixed lifetime.
Its Certificate documentation
explains renewal scheduling.
kubectl -n applications get certificate web-production \
-o jsonpath='{.status.renewalTime}{"\n"}{.status.notAfter}{"\n"}'For an intentional staging renewal test, record status.revision and
status.notAfter, run cmctl renew web-staging -n applications, then watch
CertificateRequests, Orders, and Challenges. Confirm a new successful issuance
and the updated revision and expiry. An existing Ready=True can remain while
renewal is underway, so kubectl wait ... Ready alone is not proof of renewal.
The cmctl reference
documents manual renewal and cmctl status certificate diagnostics.
After validation, confirm the test's challenge TXT value is removed. Other challenges may legitimately use the same owner; do not delete the entire TXT record set to clean up one value.
Rotate credentials and upgrade one component at a time
Create a replacement token with the same reviewed scope. Update the appropriate Secret through your existing secret-management process, retaining its name and key. ExternalDNS loads its token at startup, so restart it after the Secret update:
kubectl -n external-dns rollout restart deployment/external-dns
kubectl -n external-dns rollout status deployment/external-dns --timeout=180sThe DNScale cert-manager webhook reads its referenced Secret for each Present and CleanUp request. Test a staging issuance and cleanup after updating that Secret. If the CA reuses an existing authorization, use a fresh test hostname covered by the token to exercise DNS-01 again. For ExternalDNS, verify a controlled record change in its test scope. Revoke the old tokens after both workflows succeed. If a GitOps controller or external secret store owns these resources, update that source of truth so it does not restore the old configuration.
Before upgrades, record release versions, image digests, Helm values, API token scope, owner ID, and DNS exports. Protect backups of TLS Secrets and ACME account keys as credentials. Upgrade a single controller or webhook at a time, following its release notes and CRD upgrade instructions. Preserve domain and zone filters, source namespace, TXT prefix, owner ID, and deletion policy.
In a disposable scope, check a DNS target update, a staging renewal, selective TXT cleanup, and restart recovery. For the cert-manager webhook, also verify:
kubectl wait apiservice/v1alpha1.acme.dnscale.eu \
--for=condition=Available --timeout=60sA Helm rollback restores Kubernetes release configuration. It does not restore old DNS records, undo issued certificates, or necessarily reverse CRD changes. Compare actual DNS and certificate state with the saved baseline after any rollback.
Recover at the layer that failed
Use the symptom to choose where to investigate:
| Symptom | Check first | Recovery direction |
|---|---|---|
| Application DNS missing | Ingress status, namespace/label filters, both dry-run settings, controller logs | Correct selection or credentials, then let ExternalDNS reconcile |
| Ownership conflict | Another writer, foreign TXT owner, disabled records | Pause the competing writer and review ownership before resuming |
| Challenge pending | Issuer readiness, webhook APIService, Secret access, authoritative TXT, CAA | Fix the failed prerequisite; inspect the ACME events before retrying |
| DNS correct, wrong certificate served | Ingress TLS Secret, hostname/SNI, ingress class and reload | Fix serving configuration; a DNS edit will not repair the TLS Secret reference |
| Deleted application still resolves | upsert-only policy and resolver caches | Review retained records and ownership, then perform deliberate cleanup |
For ACME failures, use the upstream ACME troubleshooting workflow and the DNScale CAA debugging guide. Repeatedly forcing production issuance can exhaust CA limits without fixing the cause.
If ExternalDNS is applying unwanted changes, first pause any GitOps process that would restore its replica count, then stop reconciliation:
kubectl -n external-dns scale deployment/external-dns --replicas=0Preserve its ownership TXT while investigating. Correct the declarative application state or filters and restore only reviewed DNS changes. Resume with the same owner ID and prefix, inspect dry-run output, then re-enable writes. Do not remove ownership markers just to make a conflict disappear.
Retire applications deliberately
With upsert-only, deleting an Ingress retains its DNS. With sync, removal
can delete its application records and ownership TXT. Review the whole managed
scope before enabling sync; do not enable it across a production zone merely
to clean up one hostname.
Drain traffic and account for the previous DNS TTL before dismantling an old endpoint. Remove the application's desired DNS resource, verify the chosen cleanup behavior at authoritative servers, and allow resolver caches to expire. The explicit Certificate in this guide has its own lifecycle: removing the Ingress does not remove that Certificate. Retire it and its unused TLS Secret under your retention policy once no workload needs them. Leave shared issuers, other applications' credentials, and unrelated records in place.
For the broader change and recovery process, use DNS Operations and DNS TTL best practices.
Frequently asked questions
- Can ExternalDNS and cert-manager use the same DNScale zone?
- Yes. Give them separate API tokens and separate record ownership. ExternalDNS manages application A, AAAA, or CNAME records and its edns-prefixed TXT markers; cert-manager manages _acme-challenge TXT values. Keep Terraform, DNSControl, and manual writers outside the controller-owned record sets.
- Does a working certificate prove application DNS is correct?
- No. DNS-01 validates a challenge TXT record. Application DNS, ingress routing, and the certificate served to clients need their own checks. A Certificate can be Ready while the endpoint still serves an older Secret or routes incorrectly.
- Does a Helm rollback restore DNS and certificates?
- No. It restores a release's Kubernetes configuration, not earlier DNScale records or certificate issuance. Record previous DNS values, ownership, versions, and configuration before a change, then verify the actual state after rollback.
- Can ExternalDNS v1.0.0 publish wildcard DNS names?
- The DNScale ExternalDNS webhook v1.0.0 does not support wildcard DNS names. cert-manager can validate wildcard certificates through DNS-01; that is separate from publishing application DNS. Use supported explicit hostnames for ExternalDNS.
Connect certificate validation with DNS
Follow the DNS-01 guide to understand the TXT records used for certificate issuance.
Read the DNS-01 guide