caddyserver/caddy · error

storing ECH private key: %v

Error message

storing ECH private key: %v

What it means

Caddy persists each new ECH config as three objects under ech/configs/<configID>/ in the configured storage: key.bin (HPKE private key), config.bin, and meta.json. This is the first Store call; it fails when the storage backend rejects the write — permissions, disk full, read-only filesystem, or a remote storage provider error (auth, network, quota). Because it is the first of three writes, a failure here leaves no partial config behind.

Source

Thrown at modules/caddytls/ech.go:682

	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH private key: %v", err)
	}
	echConfigBytes, err := echCfg.MarshalBinary()
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH config: %v", err)
	}
	metaBytes, err := json.Marshal(meta)
	if err != nil {
		return echConfig{}, fmt.Errorf("marshaling ECH config metadata: %v", err)
	}

	parentKey := path.Join(echConfigsKey, strconv.Itoa(int(configID)))
	keyKey := path.Join(parentKey, "key.bin")
	configKey := path.Join(parentKey, "config.bin")
	metaKey := path.Join(parentKey, "meta.json")

	if err := ctx.Storage().Store(ctx, keyKey, privKeyBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH private key: %v", err)
	}
	if err := ctx.Storage().Store(ctx, configKey, echConfigBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH config: %v", err)
	}
	if err := ctx.Storage().Store(ctx, metaKey, metaBytes); err != nil {
		return echConfig{}, fmt.Errorf("storing ECH config metadata: %v", err)
	}

	echCfg.privKeyBin = privKeyBytes
	echCfg.configBin = echConfigBytes // this contains the public key
	echCfg.meta = meta

	return echCfg, nil
}

// ECH represents an Encrypted ClientHello configuration.
//
// EXPERIMENTAL: Subject to change.

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Verify write access of the storage path/backend: have Caddy's user create a file under the storage prefix (default data dir, or your storage module's location).
  2. Free disk space or fix quota on the storage volume.
  3. For remote storage modules, check credentials and connectivity (the module usually logs a more specific error just before).
  4. Restart Caddy after fixing storage; provisioning will retry and find a free config ID.

Example fix

// before: storage dir not writable
$ ls -ld /var/lib/caddy
 drwx------ root root

// after
$ chown -R caddy:caddy /var/lib/caddy
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: ensure the storage prefix is writable.
func probeECHStorageWrite(ctx context.Context, stor caddy.Storage) error {
    return stor.Store(ctx, "ech/configs/.probe", []byte("ok"))
}

Try / catch

if err != nil && strings.Contains(err.Error(), "storing ECH private key") {
    // storage-level failure: surface wrapped cause, fix backend, then restart to retry
    log.Printf("ECH store failed, check storage: %v", err)
}

Prevention

When it happens

Trigger: ECH provisioning triggered (first start with ech on, or all existing ECH configs expired/rotated) while ctx.Storage().Store fails: read-only volume, exhausted disk, s3/restic/redis storage auth failure or network outage.

Common situations: Running Caddy in a container with a read-only or unmounted data volume; file_storage directory owned by root while Caddy drops privileges; disk-full nodes; distributed storage briefly unavailable during certificate/ECH provisioning.

Related errors


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