caddyserver/caddy · critical

saving root key: %v

Error message

saving root key: %v

What it means

The final step of genRoot stores the root key PEM at storageKeyRootKey. A storage write failure here aborts CA setup with 'saving root key'. Note the root cert may already be stored at this point, leaving intentionally-recoverable-but-inconsistent assets (see the TODO in this file about all-or-none assets), so the next start will find a cert without a key unless cleaned up.

Source

Thrown at modules/caddypki/ca.go:339

	rootCert, rootKey, err = generateRoot(repl.ReplaceAll(ca.RootCommonName, ""))
	if err != nil {
		return nil, nil, fmt.Errorf("generating CA root: %v", err)
	}
	rootCertPEM, err := pemEncodeCert(rootCert.Raw)
	if err != nil {
		return nil, nil, fmt.Errorf("encoding root certificate: %v", err)
	}
	err = ca.storage.Store(ca.ctx, ca.storageKeyRootCert(), rootCertPEM)
	if err != nil {
		return nil, nil, fmt.Errorf("saving root certificate: %v", err)
	}
	rootKeyPEM, err := certmagic.PEMEncodePrivateKey(rootKey)
	if err != nil {
		return nil, nil, fmt.Errorf("encoding root key: %v", err)
	}
	err = ca.storage.Store(ca.ctx, ca.storageKeyRootKey(), rootKeyPEM)
	if err != nil {
		return nil, nil, fmt.Errorf("saving root key: %v", err)
	}

	return rootCert, rootKey, nil
}

func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey crypto.Signer) (interCertChain []*x509.Certificate, interKey crypto.Signer, err error) {
	var interCert *x509.Certificate
	interCertPEM, err := ca.storage.Load(ca.ctx, ca.storageKeyIntermediateCert())
	if err != nil {
		if !errors.Is(err, fs.ErrNotExist) {
			return nil, nil, fmt.Errorf("loading intermediate cert: %v", err)
		}

		// TODO: should we require that all or none of the assets are required before overwriting anything?
		interCert, interKey, err = ca.genIntermediate(rootCert, rootKey)
		if err != nil {
			return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err)
		}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Fix storage writability/capacity, then delete the partially provisioned CA directory in storage (cert without key) and restart so root generation re-runs atomically.
  2. Monitor storage free space so provisioning cannot run out mid-write.
  3. For custom backends, ensure writes are atomic and idempotent across restarts.
  4. If assets must be kept, manually place a matching key.pem; otherwise regenerating is safer.
Defensive patterns

Strategy: retry

Validate before calling

// After provisioning attempts, detect partial state (cert without key)
_, e1 := os.Stat(caDir + "/root.crt")
_, e2 := os.Stat(caDir + "/key.pem")
if (e1 == nil) != (e2 == nil) { cleanupPartialCA(caDir) }

Type guard

func caStorageConsistent(dir string) bool {
    _, c := os.Stat(filepath.Join(dir, "root.crt"))
    _, k := os.Stat(filepath.Join(dir, "key.pem"))
    return (c == nil) == (k == nil)
}

Try / catch

if strings.Contains(err.Error(), "saving root key") {
    // free space / fix backend, delete partial CA dir, restart to regenerate cleanly
}

Prevention

When it happens

Trigger: storage.Store(key) fails after Store(cert) succeeded: disk fills up mid-provisioning, backend drops between two writes, permissions change. First-boot-of-CA path only.

Common situations: Disk-full or quota-hit exactly during provisioning; flaky network storage; partially provisioned CA left behind after a crash, causing confusing follow-up errors (e.g. 586/587) on next start.

Related errors


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