Email for your domain, with DNS setup handled. PostScale

    Sign Up

    Kubernetes DNS and TLS operations with DNScale

    9 min readIntermediate

    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.

    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:

    ResourceOwnerExample
    Application A, AAAA, or CNAMEExternalDNSweb.k8s.example.org
    Application ownership TXTExternalDNSedns-a.web.k8s.example.org for an A record
    ACME challenge TXT valuescert-manager through its DNScale webhook_acme-challenge.web.k8s.example.org
    TLS Secretcert-managerapplications/web-production-tls
    Load-balancer address and served TLS configurationYour ingress controllerIngress applications/web
    Zone delegation, CAA, and other zone policyYour DNS operations processNS 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:

    ComponentVersion
    Kubernetes1.35.0
    ExternalDNS controller / official chartv0.23.0 / 1.23.0
    DNScale ExternalDNS webhook imagev1.0.0
    cert-manager1.19.3
    DNScale cert-manager webhook image / chartv1.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-dnscale is a ready ClusterIssuer from the cert-manager guide.
    • A working HTTP Service named web exists on port 80 in applications.
    • Your ingress controller publishes a reachable address in Ingress status.
    • ExternalDNS watches applications with labelFilter: external-dns=enabled and starts with both DNSCALE_DRY_RUN=true and extraArgs.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: 80

    This 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=100

    Review 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 +short

    Expect 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-tls

    Staging 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 -dates

    Use 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.

    SignalOperational response
    Repeated ExternalDNS or DNScale API failuresInspect both containers' logs, token scope, ownership conflicts, and API throttling
    Ingress address absent or unexpectedCheck the ingress controller and load balancer before editing DNS
    Certificate renewal overdue or expiry approachingInspect Certificate, CertificateRequest, Order, and Challenge events
    Certificate Ready but HTTPS wrongCheck hostname resolution, ingress class, TLS Secret reference, and controller reload
    Metrics scrape missingRepair 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 < 14

    Fourteen 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=180s

    The 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=60s

    A 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:

    SymptomCheck firstRecovery direction
    Application DNS missingIngress status, namespace/label filters, both dry-run settings, controller logsCorrect selection or credentials, then let ExternalDNS reconcile
    Ownership conflictAnother writer, foreign TXT owner, disabled recordsPause the competing writer and review ownership before resuming
    Challenge pendingIssuer readiness, webhook APIService, Secret access, authoritative TXT, CAAFix the failed prerequisite; inspect the ACME events before retrying
    DNS correct, wrong certificate servedIngress TLS Secret, hostname/SNI, ingress class and reloadFix serving configuration; a DNS edit will not repair the TLS Secret reference
    Deleted application still resolvesupsert-only policy and resolver cachesReview 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=0

    Preserve 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