caddyserver/caddy · error

intermediate certificate lifetime must be less than actual r

Error message

intermediate certificate lifetime must be less than actual root certificate lifetime (%s)

What it means

Caddy's PKI app validates that a generated intermediate certificate's lifetime is strictly shorter than the time remaining on the root certificate it will be signed by. If IntermediateLifetime (a caddy.Duration, e.g. in the pki app config) is >= time.Until(rootCert.NotAfter), CA initialization aborts. This prevents issuing an intermediate that outlives its issuer, which clients would reject.

Source

Thrown at modules/caddypki/ca.go:172

		rootCertChain, rootKey, err = ca.Root.Load()
		if err != nil {
			return err
		}
		rootCert = rootCertChain[0]
	} else {
		ca.rootCertPath = "storage:" + ca.storageKeyRootCert()
		rootCert, rootKey, err = ca.loadOrGenRoot()
	}
	if err != nil {
		return err
	}

	if ca.Intermediate != nil {
		interCertChain, interKey, err = ca.Intermediate.Load()
	} else {
		actualRootLifetime := time.Until(rootCert.NotAfter)
		if time.Duration(ca.IntermediateLifetime) >= actualRootLifetime {
			return fmt.Errorf("intermediate certificate lifetime must be less than actual root certificate lifetime (%s)", actualRootLifetime)
		}

		interCertChain, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
	}
	if err != nil {
		return err
	}

	ca.mu.Lock()
	ca.root, ca.interChain, ca.interKey = rootCert, interCertChain, interKey
	ca.mu.Unlock()

	return nil
}

// RootCertificate returns the CA's root certificate (public key).
func (ca CA) RootCertificate() *x509.Certificate {
	ca.mu.RLock()

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Set intermediate_lifetime in the pki CA config to a value clearly below the root's remaining lifetime (e.g. root remaining 8760h -> intermediate_lifetime 720h), then restart Caddy.
  2. Check the actual root expiry: caddy trust / inspect the root cert in storage (storage/caddy/pki/<id>/ca/root.crt) with openssl x509 -enddate; if it is nearly expired, import a fresh root or delete the CA assets so Caddy regenerates them.
  3. If the root is external and short-lived, use the intermediate {} block to supply a pre-made intermediate with a suitable lifetime instead of letting Caddy generate one.
  4. If you intended sign_with_root semantics, set sign_with_root so no generated intermediate is required.

Example fix

// before (caddy.json, apps.pki.cAs[0])
{"id":"local","intermediate_lifetime":14400000000000}
// after
{"id":"local","intermediate_lifetime":3600000000000}
Defensive patterns

Strategy: validation

Validate before calling

// Go, before relying on a CA: derive the root's remaining lifetime and compare
root := getRootCert(t) // *x509.Certificate you distribute/import
remaining := time.Until(root.NotAfter)
desired := 12 * time.Hour
if desired >= remaining {
    desired = remaining / 2 // always strictly below
}
_ = desired // use as intermediate_lifetime

Type guard

func intermediateLifetimeOK(rootRemaining, intermediate time.Duration) bool {
    return intermediate > 0 && intermediate < rootRemaining
}

Prevention

When it happens

Trigger: The root certificate was imported or has already run part of its life, and the configured intermediate_lifetime equals or exceeds the root's remaining validity. For example a root with 10 days left and intermediate_lifetime 144h (default 7d is fine, but explicit 12h*20 or 24h*10 = 240h would not be). Reproduce: pki { ca { root { cert ...key... } intermediate_lifetime huge } } then start Caddy.

Common situations: Using a short-lived external root cert (e.g. from an internal corporate CA) while keeping the default or a large intermediate_lifetime; importing an old root whose NotAfter is close; downgrading root lifetime to 1h in tests while leaving intermediate_lifetime at 24h+.

Understand the failure class

Related errors


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