caddyserver/caddy · error

loading root key: %v

Error message

loading root key: %v

What it means

Emitted by renewCertsForCA (modules/caddypki/maintain.go:87) during the PKI maintenance loop when an intermediate certificate is nearing expiry and loadOrGenRoot fails to load or generate the root key. The wrapped error carries the real cause: an unreadable/corrupt root key file, a PEM decode failure, or a storage backend error.

Source

Thrown at modules/caddypki/maintain.go:87

	if ca.Root == nil {
		if ca.needsRenewal(ca.root) {
			// TODO: implement root renewal (use same key)
			log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)",
				zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)),
			)
		}
	}

	// only maintain the intermediate if it's not manually provided in the config
	if ca.Intermediate == nil {
		if ca.needsRenewal(ca.interChain[0]) {
			log.Info("intermediate expires soon; renewing",
				zap.Duration("time_remaining", time.Until(ca.interChain[0].NotAfter)),
			)

			rootCert, rootKey, err := ca.loadOrGenRoot()
			if err != nil {
				return fmt.Errorf("loading root key: %v", err)
			}
			interCert, interKey, err := ca.genIntermediate(rootCert, rootKey)
			if err != nil {
				return fmt.Errorf("generating new certificate: %v", err)
			}
			ca.interChain, ca.interKey = []*x509.Certificate{interCert}, interKey

			log.Info("renewed intermediate",
				zap.Time("new_expiration", ca.interChain[0].NotAfter),
			)
		}
	}

	return nil
}

// needsRenewal reports whether the certificate is within its renewal window
// (i.e. the fraction of lifetime remaining is less than or equal to RenewalWindowRatio).

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Inspect the wrapped error for the root cause: fix file permissions (chown/chmod) on the root key under <storage>/pki/authorities/<id>/root.key
  2. If the root key was lost, delete the CA's storage subtree (root + intermediate) so Caddy generates a fresh root and intermediate, and re-install the new root CA in client trust stores
  3. Verify storage backend health: run caddy start with local storage or check the Redis/S3 plugin connectivity
  4. Restore the exact root key from a backup if clients already trust the root, then restart Caddy so the maintenance loop retries

Example fix

# before: root key unreadable (permissions)
ls -l /var/lib/caddy/pki/authorities/local/root.key   # -rw------- root root
# systemd service runs as caddy

# after: fix ownership, restart, let the maintenance loop retry
chown -R caddy:caddy /var/lib/caddy/pki
systemctl restart caddy
Defensive patterns

Strategy: retry

Validate before calling

// probe storage health and file access before relying on the maintenance loop
func canReadRootKey(storageDir, caID string) error {
	p := filepath.Join(storageDir, "pki", "authorities", caID, "root.key")
	f, err := os.Open(p)
	if err != nil {
		return fmt.Errorf("root key unreadable: %w", err)
	}
	return f.Close()
}

Try / catch

// renewCertsForCA runs on every maintenance tick; log and let it retry,
// but escalate after repeated failures
if err := p.renewCertsForCA(ca); err != nil {
    p.log.Error("renewing intermediate certificates",
        zap.Error(err), zap.String("ca", ca.ID))
    // do not crash; the ticker will retry next interval. Alert if it persists
    // past the certificate's remaining lifetime.
}

Prevention

When it happens

Trigger: The intermediate needs renewal (ca.needsRenewal true) and ca.loadOrGenRoot() errors: root key missing from the data directory, wrong permissions on the storage folder, corrupted PEM file, key/cert mismatch in a manually-formatted keystore, or a failing distributed storage plugin (Redis, S3, etc.) used as Caddy storage.

Common situations: Storage directory deleted or partially restored from backup (root cert present, key gone); containers running as a user without read access to /data/caddy/pki; disk-full events truncating key files; a flaky external storage backend; SELinux denying reads.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/411c1a4b78f9249e. Report an issue: GitHub.