Skip to content

Approach to fix "Your DNS resolver isn't doing DNSSEC validation" #405

@pgmillon

Description

@pgmillon

Hi,
This is just some knowledge sharing to help people seeking some guidance when having a DNSSEC issue with Mailu:

Your DNS resolver at 10.32.0.10 isn't doing DNSSEC validation; Please use another resolver or enable unbound

I'm running Mailu using this helm chart on Kubernetes by Scaleway and as of today and as far as I understand it, the cluster resolver is not validating DNSSEC.
sources:

root@debug:/# dig www.example.org. A +adflag

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> www.example.org. A +adflag
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7369
;; flags: qr rd; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: 72266556e2462318 (echoed)
;; QUESTION SECTION:
;www.example.org.               IN      A

;; ANSWER SECTION:
www.example.org.        30      IN      CNAME   www.example.org-v2.edgesuite.net.
www.example.org-v2.edgesuite.net. 30 IN CNAME   a1519.dscr.akamai.net.
a1519.dscr.akamai.net.  30      IN      A       92.123.239.35
a1519.dscr.akamai.net.  30      IN      A       92.123.239.75

;; Query time: 38 msec
;; SERVER: 10.32.0.10#53(10.32.0.10) (UDP)
;; WHEN: Sun May 04 20:57:11 UTC 2025
;; MSG SIZE  rcvd: 258

I opened a ticket to check with my provider if we could make DNSSEC validation work on the upstream resolver for my cluster but in the meantime I needed to fix my broken admin pod.

Since I run a combination of Terraform and Kustomize with Helm, here's what I did.

First, get the coredns IP and all the pod subnets within my cluster and use them to template the unbound config file:

data "kubernetes_nodes" "nodes" {}
data "kubernetes_service" "coredns" {
  metadata {
    name      = "coredns"
    namespace = "kube-system"
  }
}

resource "kubernetes_config_map" "unbound" {
    metadata {
        name      = "unbound-config"
        namespace = "mail-system"
    }

    data = {
        "unbound.conf" = templatefile("${path.module}/files/unbound.conf.tftpl", {
            subnets = tolist(data.kubernetes_nodes.nodes.nodes[*].spec[0].pod_cidr)
            dns_ip  = data.kubernetes_service.coredns.spec[0].cluster_ip
        })
    }
}
server:
  verbosity: 1
  interface: 0.0.0.0
  logfile: ""
  do-ip4: yes
  do-ip6: no
  do-udp: yes
  do-tcp: yes
  do-daemonize: no
%{ for subnet in subnets ~}
  access-control: ${subnet} allow
%{ endfor ~}
  directory: "/etc/unbound"
  username: unbound
  auto-trust-anchor-file: trusted-key.key
  root-hints: "/etc/unbound/root.hints"
  hide-identity: yes
  hide-version: yes
  cache-min-ttl: 300

  domain-insecure: "cluster.local"
  private-domain: "cluster.local"
  local-zone: ".172.in-addr.arpa." nodefault
  stub-zone:
    name: "cluster.local"
    stub-addr: ${dns_ip}
  forward-zone:
    name: "."
    forward-addr: ${dns_ip}

source: #144 (comment)

Then, add a resolver service using Mailu's image:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mailu-resolver
  namespace: mail-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/component: resolver
      app.kubernetes.io/instance: main
      app.kubernetes.io/name: mailu
  template:
    metadata:
      labels:
        app.kubernetes.io/component: resolver
        app.kubernetes.io/instance: main
        app.kubernetes.io/name: mailu
    spec:
      containers:
        - name: resolver
          image: ghcr.io/mailu/unbound:2024.06.10
          command:
            - /usr/sbin/unbound
          volumeMounts:
            - mountPath: /etc/unbound/unbound.conf
              name: unbound-config
              subPath: unbound.conf
          ports:
            - name: dns-udp
              containerPort: 53
              protocol: UDP
            - name: dns-tcp
              containerPort: 53
              protocol: TCP
          readinessProbe:
            tcpSocket:
              port: 53
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            tcpSocket:
              port: 53
            initialDelaySeconds: 15
            periodSeconds: 20
      volumes:
        - name: unbound-config
          configMap:
            name: unbound-config
---

apiVersion: v1
kind: Service
metadata:
  name: mailu-resolver
  namespace: mail-system
  labels:
      app.kubernetes.io/component: resolver
      app.kubernetes.io/instance: main
      app.kubernetes.io/name: mailu
spec:
    type: ClusterIP
    ports:
      - name: dns-udp
        port: 53
        protocol: UDP
      - name: dns-tcp
        port: 53
        protocol: TCP
    selector:
        app.kubernetes.io/component: resolver
        app.kubernetes.io/instance: main
        app.kubernetes.io/name: mailu

source: https://github.com/Mailu/Mailu/blob/2024.06.10/setup/flavors/compose/docker-compose.yml#L56-L67

And finally, where Kustomize became very useful to tweak the admin deployment and add an initContainer:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: mail-system

helmCharts:
  - name: mailu
    includeCRDs: false
    valuesFile: values.yml
    releaseName: main
    namespace: mail-system
    version: 2.1.2
    repo: https://mailu.github.io/helm-charts/

resources:
  - resolver.yml

patches:
  - path: patch-admin.yml

patch-admin.yml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mailu-admin
  namespace: mail-system
spec:
  template:
    spec:
      initContainers:
        - name: configure-dns
          image: ubuntu
          command:
            - sh
            - -c
            - |
              RESOLVER_IP=$(getent hosts mailu-resolver | awk '{ print $1 }')
              sed "s/^nameserver .*/nameserver $RESOLVER_IP/" /etc/resolv.conf > /tmp/resolv.conf
              cat /tmp/resolv.conf > /etc/resolv.conf
              cat /etc/resolv.conf

It's definitively not ideal and I agree the upstream resolver should be fixed to properly support DNSSEC validation. But for now, this does the job.

I saw some comments recommending to run our own recursive resolver for safety reasons.
Maybe adding unbound to this chart as an option could be a nice idea.

I hope this will be useful to anyone.

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    StaleIssues that have not been updated for a whileenhancementImprovements or additions to existing featureshelp wantedIssues that need assistance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions