coredns/coredns · error

no SOA RRSIG found in first 100 records

Error message

no SOA RRSIG found in first 100 records

What it means

resign() scans the first 100 records of a signed zone looking for the RRSIG covering the SOA record. If none is found within that limit it gives up and returns this error, because the plugin cannot determine signature freshness for the zone's SOA.

Source

Thrown at plugin/sign/signer.go:160

			if now.Sub(incep) >= 0 && now.Sub(incep) > durationResignDays {
				return fmt.Errorf("inception %q was more than: %s ago from %s: %s", incep.Format(timeFmt), durationResignDays, now.Format(timeFmt), now.Sub(incep))
			}
			// Inception hasn't even start yet.
			if now.Sub(incep) < 0 {
				return fmt.Errorf("inception %q date is in the future: %s", incep.Format(timeFmt), now.Sub(incep))
			}

			expire, _ := time.Parse("20060102150405", dns.TimeToString(x.Expiration))
			if expire.Sub(now) < durationExpireDays {
				return fmt.Errorf("expiration %q is less than: %s away from %s: %s", expire.Format(timeFmt), durationExpireDays, now.Format(timeFmt), expire.Sub(now))
			}
		}
		i++
		if i > 100 {
			// 100 is a random number. A SOA record should be the first in the zonefile, but RFC 1035 doesn't actually mandate this. So it could
			// be 3rd or even later. The number 100 looks crazy high enough that it will catch all weird zones, but not high enough to keep the CPU
			// busy with parsing all the time.
			return fmt.Errorf("no SOA RRSIG found in first 100 records")
		}
	}

	return zp.Err()
}

func signAndLog(s *Signer, why error) {
	now := time.Now().UTC()
	z, err := s.Sign(now)
	log.Infof("Signing %q because %s", s.origin, why)
	if err != nil {
		log.Warningf("Error signing %q with key tags %q in %s: %s, next: %s", s.origin, keyTag(s.keys), time.Since(now), err, now.Add(durationRefreshHours).Format(timeFmt))
		return
	}

	if err := s.write(z); err != nil {
		log.Warningf("Error signing %q: failed to move zone file into place: %s", s.origin, err)
		return

View on GitHub (pinned to 558c9757a9)

Solutions

  1. Re-sign the zone so an SOA RRSIG exists (or sign it at all)
  2. Move the SOA record and its RRSIG to the top of the zone file (RFC 1035 convention)
  3. Validate the zone file with named-checkzone or dnssec-verify
  4. Ensure the correct zone file is loaded

Example fix

// before (SOA RRSIG missing)
example.org. 3600 IN SOA ns1 ...
example.org. 3600 IN NS ns1 ...
// after: re-sign so the SOA carries an RRSIG
example.org. 3600 IN RRSIG SOA 8 2 3600 ... (sig)
example.org. 3600 IN SOA ns1 ...
Defensive patterns

Strategy: validation

Validate before calling

func hasSOARRSIG(z *zone.File) bool {
	for _, rr := range z.RR {
		if r, ok := rr.(*dns.RRSIG); ok && r.TypeCovered == dns.TypeSOA {
			return true
		}
	}
	return false
}

Try / catch

if err := resign(zoneFile); err != nil {
	if strings.Contains(err.Error(), "no SOA RRSIG") {
		log.Printf("zone not signed or malformed: %v", err)
		// run full sign instead of resign check
	}
}

Prevention

When it happens

Trigger: resign() is called on a zone whose first 100 records contain no RRSIG with TypeCovered == dns.TypeSOA (no SOA RRSIG at all, or SOA pushed beyond the 100th record).

Common situations: Serving an unsigned zone file through the sign plugin's resign check path; a malformed/unsorted zone file with the SOA buried past 100 records; hand-edited zone missing its SOA RRSIG.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of coredns/coredns@558c9757a9 (2026-09-06). Data as JSON: /api/errors/2510424a3d304000. Report an issue: GitHub.