netbirdio/netbird · error

write public key file (%s): %w

Error message

write public key file (%s): %w

What it means

Returned by the signer CLI's create-root-key command when os.WriteFile cannot persist the generated public key PEM to the path given with --pub-key-file (mode 0600). It wraps the underlying *fs.PathError. Note the private key file is written before this step, so hitting this error leaves a freshly written private key on disk with no matching public key beside it.

Source

Thrown at client/cmd/signer/rootkey.go:68

	if err := createRootKeyCmd.MarkFlagRequired("expiration"); err != nil {
		panic(err)
	}
}

func handleGenerateRootKey(cmd *cobra.Command, privKeyFile, pubKeyFile string, expiration time.Duration) error {
	rk, privPEM, pubPEM, err := reposign.GenerateRootKey(expiration)
	if err != nil {
		return fmt.Errorf("generate root key: %w", err)
	}

	// Write private key
	if err := os.WriteFile(privKeyFile, privPEM, 0o600); err != nil {
		return fmt.Errorf("write private key file (%s): %w", privKeyFile, err)
	}

	// Write public key
	if err := os.WriteFile(pubKeyFile, pubPEM, 0o600); err != nil {
		return fmt.Errorf("write public key file (%s): %w", pubKeyFile, err)
	}

	cmd.Printf("%s\n\n", rk.String())
	cmd.Printf("✅ Root key pair generated successfully.\n")
	return nil
}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Verify the parent directory of --pub-key-file exists and is writable (mkdir -p); the tool does not create directories.
  2. Keep both key files in the same prepared directory so one permission fix covers both writes.
  3. Free disk space if ENOSPC, or remount the volume read-write if EROFS.
  4. If a partial run left a private key behind, delete it before re-running so you do not accumulate an orphaned key.

Example fix

# before
signer create-root-key --priv-key-file ./keys/root.priv --pub-key-file /etc/signer/root.pub --expiration 8760h
# -> write public key file (/etc/signer/root.pub): open ...: no such file or directory

# after
mkdir -p ./keys
signer create-root-key --priv-key-file ./keys/root.priv --pub-key-file ./keys/root.pub --expiration 8760h
Defensive patterns

Strategy: validation

Validate before calling

// ensure BOTH output paths are writable before generating anything
paths := []string{privKeyFile, pubKeyFile}
for _, p := range paths {
	if dir := filepath.Dir(p); dir != "" {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			log.Fatalf("prepare dir for %s: %v", p, err)
		}
	}
	f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE, 0o600)
	if err != nil {
		log.Fatalf("path not writable: %s (%v)", p, err)
	}
	f.Close()
}

Try / catch

if err := handleGenerateRootKey(cmd, priv, pub, exp); err != nil {
	var pathErr *fs.PathError
	if errors.As(err, &pathErr) {
		// both key writes produce *fs.PathError; distinguish ENOSPC/EROFS/EACCES
		// and remember the priv file may exist from a partial run
	}
}

Prevention

When it happens

Trigger: Running `signer create-root-key` where the --pub-key-file path is in a missing/unwritable directory, on a read-only filesystem, with no space left, or where the target is a directory or an unwritable existing file — while the --priv-key-file path is writable.

Common situations: The two flags pointing at different directories where only the private key's directory was prepared; typos in the public key path; CI artifacts directory not created before the step runs; disk filling up between the two writes.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/dec635f176d12139. Report an issue: GitHub.