Last modified September 28, 2026

Advanced CoreDNS configuration

Your Giant Swarm clusters come with a default configuration for the CoreDNS add-on. CoreDNS is installed as default application in your clusters using a HelmRelease. This guide explains how you can customize the CoreDNS configuration in your clusters.

Before you start

This guide describes the zone-aware coredns.* values interface. You need cluster release v35.0.1 or newer, which ships coredns-app 1.33.0 on every provider. On older releases, see migrating from the previous interface for the keys that apply to you.

Where to store the user configuration

Given the cluster you are trying to configure is called 123ab, you can create or extend the <CLUSTER>-userconfig configmap of the organization namespace on the management cluster. Inside the configmap, the Helm values are passed as the field called values:

data:
  values: |
    global:
      apps:
        coreDns:
          values:
            coredns:
              ...
kind: ConfigMap
metadata:
  name: 123ab-userconfig
  namespace: org-my-org

Read more about customizing default apps.

Every example on this page shows only the values payload, so you have to nest it under global.apps.coreDns.values as in the snippet.

How the configuration is organized

CoreDNS serves DNS through server blocks, one per zone. The chart renders three kinds of blocks, and each one is configured under its own key:

KeyServer blockPurpose
coredns.public.Everything that isn’t a local zone. Resolved through the forward plugin.
coredns.clustercluster.localIn-cluster service discovery. Resolved through the kubernetes plugin.
coredns.additionalZonesone per entryExtra zones you define yourself.
coredns.customrawA Corefile snippet appended verbatim.

The important change compared to the previous interface: cache, log, and load balancing are configured per zone, not globally. Each of coredns.public, coredns.cluster, and every coredns.additionalZones entry carries its own cache, log, and loadbalance. So you can cache external answers for 5 minutes while keeping in-cluster answers fresh, or log every query for one zone only.

Zones that omit these keys fall back to the deprecated global keys first, then to the built-in defaults. So cache.success.ttl falls back to configmap.cache and then to 30 seconds, log falls back to configmap.log and then to denial plus error, and loadbalance falls back to loadbalancePolicy and then to round_robin. The remaining cache defaults are a capacity of 9984 entries per zone and denial 9984 5 for negative answers.

Cache settings

Cache lives under <zone>.cache. Positive and negative answers are tuned separately, each with a capacity and a time to live (TTL) in seconds. To raise the TTL of external answers to 60 seconds while leaving in-cluster answers at the default:

coredns:
  public:
    cache:
      success:
        ttl: 60

To use a different TTL per zone:

coredns:
  public:
    cache:
      success:
        capacity: 9984
        ttl: 60
      denial:
        capacity: 9984
        ttl: 10
  cluster:
    cache:
      success:
        ttl: 5

The chart also exposes prefetch, serveStale, servfail, disable, keepttl, ttl, and zones. For example, to fetch popular records again before they expire and serve stale answers while they refresh:

coredns:
  public:
    cache:
      prefetch:
        amount: 2
        duration: 1m
      serveStale:
        enabled: true
        duration: 1h
        refreshMode: immediate

The cache plugin supports much more detailed configuration, which is documented in the upstream documentation.

Logs

By default we log the denial and error classes. log is a list of classes, set per zone:

coredns:
  public:
    log:
      - all

To keep the default on external traffic but log everything in the cluster zone:

coredns:
  cluster:
    log:
      - all

Valid classes are success, denial, error, and all. To learn more about each log class, read the upstream documentation.

Load balancing

The loadbalance plugin shuffles A, AAAA, and MX records in the answer. Set it per zone to either round_robin or random:

coredns:
  cluster:
    loadbalance: random

Forwarding (formerly known as proxy)

The default forward entry we set in CoreDNS is:

forward . /etc/resolv.conf

List the upstreams under coredns.public.forward.to. Setting the list replaces the default, so include /etc/resolv.conf if you still want your cluster’s resolver in the list:

coredns:
  public:
    forward:
      to:
        - 1.1.1.1
        - /etc/resolv.conf

This sends external queries to 1.1.1.1 (Cloudflare’s DNS) and to your cluster’s default resolver. You can list several upstreams:

coredns:
  public:
    forward:
      to:
        - 1.1.1.1
        - 8.8.8.8
        - /etc/resolv.conf

Warning: The number of forward upstreams is limited to 15.

Forward plugin options are siblings of to and each maps to one directive of the upstream forward plugin:

coredns:
  public:
    forward:
      to:
        - 9.9.9.9
      policy: round_robin
      forceTCP: true
      maxFails: 3
      healthCheck: 5s
      except:
        - example.internal

The full set of supported options is except, forceTCP, preferUDP, expire, maxIdleConns, maxFails, maxConnectAttempts, dohMethod, tls, tlsServername, policy, healthCheck, maxConcurrent, next, nextOnNodata, failfastAllUnhealthyUpstreams, failover, and resolver. Leave a key unset to keep the CoreDNS default.

The cluster zone

coredns.cluster configures the cluster.local server block. The zone names and the reverse (PTR) ranges come from your cluster, so you don’t normally need to set them:

coredns:
  cluster:
    domains:
      - cluster.local
    serviceCIDR: 172.31.0.0/16
    podCIDR: 192.168.0.0/16

Kubernetes plugin parameters live under coredns.cluster.kubernetes. To expose only a subset of namespaces and return NXDOMAIN for services without ready endpoints:

coredns:
  cluster:
    kubernetes:
      namespaces:
        - default
        - production
      ignoreEmptyService: true
      ttl: 30

Warning: namespaces restricts CoreDNS to the namespaces you list. Every namespace you leave out stops resolving, including kube-system, giantswarm and flux-system, which breaks in-cluster resolution of managed components. Only set it if you know which namespaces your workloads and our managed apps need.

The supported parameters are pods, endpoint, tls, kubeconfig, apiserverQPS, apiserverBurst, apiserverMaxInflight, ttl, endpointPodNames, noendpoints, namespaces, namespaceLabels, labels, ignoreEmptyService, fallthrough, fallthroughZones, multicluster, and startupTimeout. They map to the directives of the upstream kubernetes plugin.

Additional zones

coredns.additionalZones renders one fully configured server block per entry. Each entry takes the same shape as coredns.public and coredns.cluster: names (required), plus optional cidrs, cache, forward, kubernetes, log, and loadbalance.

An entry that declares kubernetes resolves the zone through the Kubernetes API, which is what you want for a service mesh zone:

coredns:
  additionalZones:
    - names:
        - linkerd.local
      kubernetes:
        pods: verified

An entry that declares forward sends the zone to a specific upstream, with its own cache and log settings:

coredns:
  additionalZones:
    - names:
        - example.com
      forward:
        to:
          - 9.9.9.9
      cache:
        success:
          ttl: 2000
      log:
        - all

This resolves all example.com requests through a different upstream DNS resolver than the generic one, with a longer cache TTL. Because every zone is independent, adding one doesn’t affect how the other zones behave.

Raw configuration snippets

coredns.additionalZones covers most cases. When you need a directive the chart doesn’t expose, coredns.custom is appended verbatim at the end of the Corefile:

coredns:
  custom: |
    example.com:1053 {
      forward . 9.9.9.9
      cache 2000
    }

Warning: By default our clusters come with Pod Security Standards and network policies for managed components. This means the CoreDNS container doesn’t use a privileged port and listens on 1053 instead. Please make sure you test the final Corefile. We don’t take responsibility for incorrect custom configuration that could break workload communication.

Resource limits

We set resource limits for the CoreDNS deployment. The defaults are a memory limit of 1Gi, a CPU request of 250m and a memory request of 512Mi. For larger clusters these may need to be increased:

resources:
  limits:
    memory: 2Gi
  requests:
    cpu: 500m
    memory: 1Gi

Setting a value lower than the default reduces what CoreDNS gets, so check the defaults listed before you copy this example.

Migrating from the previous interface

Before coredns-app 1.31.0, Corefile settings were spread across configmap.*, loadbalancePolicy, additionalLocalZones, and cluster.*, with no way to configure a zone individually. The table below maps every old key to its replacement.

Old keyNew keyNotes
configmap.cachecoredns.public.cache.success.ttl, coredns.cluster.cache.success.ttlNow per zone. Integer, not a string. Still applies to every zone that leaves its own success TTL unset, see the note below.
configmap.logcoredns.<zone>.logNow a list of classes instead of a newline-separated string.
loadbalancePolicycoredns.<zone>.loadbalanceNow per zone.
configmap.forwardcoredns.public.forward.toNow a list of upstreams. The leading . is rendered for you, so drop it.
configmap.forwardOptionscoredns.public.forward.<option>Each option is its own typed key, for example policy or maxFails.
configmap.customcoredns.customUnchanged shape.
additionalLocalZonescoredns.additionalZonesNow a list of zone objects instead of zone-name strings.
cluster.kubernetes.clusterDomaincoredns.cluster.domainsNow a list instead of a space-separated string.
cluster.kubernetes.API.clusterIPRangecoredns.cluster.serviceCIDR
cluster.calico.CIDRcoredns.cluster.podCIDR
cluster.kubernetes.DNS.IPservice.clusterIPMoved out of the cluster topology, since it configures the DNS Service.
userIDsecurityContext.runAsUser
groupIDsecurityContext.runAsGroup
ports.prometheusports.metrics.portNow an object, matching ports.dns.
mastersInstancecontrolPlaneSame shape, clearer name.

A worked example. This is the old form:

configmap:
  cache: 60
  log: |
    all
  forward: . 1.1.1.1 8.8.8.8 /etc/resolv.conf
loadbalancePolicy: random
additionalLocalZones:
  - linkerd.local

And the same configuration in the new form:

coredns:
  public:
    cache:
      success:
        ttl: 60
    log:
      - all
    loadbalance: random
    forward:
      to:
        - 1.1.1.1
        - 8.8.8.8
        - /etc/resolv.conf
  cluster:
    cache:
      success:
        ttl: 60
    log:
      - all
    loadbalance: random
  additionalZones:
    - names:
        - linkerd.local
      kubernetes:
        pods: verified
      cache:
        success:
          ttl: 60
      log:
        - all
      loadbalance: random

Note how the old global keys have to be repeated per zone, including on the additional zone. The old configmap.log and loadbalancePolicy applied to every server block at once, so an equivalent migration has to set them on each zone you want to keep behaving the same way. That repetition is the point of the change: you’re now free to give each zone different values.

Warning: on coredns-app 1.31.0 and 1.32.0, configmap.cache, configmap.log and loadbalancePolicy were ignored without warning. The per-zone keys shipped with defaults that took precedence, so zones fell back to 30 seconds, denial plus error and round_robin whatever you set. 1.33.0 restored the fallback chain. If your cluster runs one of those two versions and you set any of the three, migrate to the per-zone keys now.

All the old keys in the table keep working until coredns-app v2, but they’re deprecated and will be removed then.

Note: before 1.31.0, configmap.cache never reached the Corefile. The old templates emitted a bare cache directive and ignored the value, so CoreDNS ran with the upstream default of 3600 seconds for positive answers no matter what you set. That’s why the key is deprecated. From 1.31.0 the cache block renders in full, so a cluster that carried a high configmap.cache sees its effective positive TTL drop to that value. Set coredns.<zone>.cache.success.ttl if your workloads depend on longer caching.

Further reading