netbirdio/netbird · error

failed to write signature file: %w

Error message

failed to write signature file: %w

What it means

The second os.WriteFile inside writeOutputFiles failed (revocation.go:216): the signature bytes could not be written to the derived path rlPath + ".sig" with 0600 permissions. Because the list file is written first, hitting this error leaves a replaced list paired with a stale or missing signature — an inconsistent pair that fails verify-revocation-list until both are regenerated together.

Source

Thrown at client/cmd/signer/revocation.go:217

	cmd.Printf("Expires At: %s\n", rl.ExpiresAt.Format(time.RFC3339))
	cmd.Printf("Number of revoked keys: %d\n", len(rl.Revoked))

	if len(rl.Revoked) > 0 {
		cmd.Println("\nRevoked Keys:")
		for keyID, revokedTime := range rl.Revoked {
			cmd.Printf("  - %s (revoked at: %s)\n", keyID, revokedTime.Format(time.RFC3339))
		}
	}

	return nil
}

func writeOutputFiles(rlPath, sigPath string, rlBytes, sigBytes []byte) error {
	if err := os.WriteFile(rlPath, rlBytes, 0o600); err != nil {
		return fmt.Errorf("failed to write revocation list file: %w", err)
	}
	if err := os.WriteFile(sigPath, sigBytes, 0o600); err != nil {
		return fmt.Errorf("failed to write signature file: %w", err)
	}
	return nil
}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Fix permissions on the stale .sig (chown/chmod) or remove it so it can be recreated
  2. Free space or remount read-write, then re-run the same create/extend command so list and signature are written as a matched pair
  3. Always verify the pair afterwards: signer verify-revocation-list ... before publishing
  4. Treat the on-disk pair as untrusted after this error until regenerated — the list may be newer than the signature

Example fix

# before: extend fails with 'failed to write signature file: open rl.json.sig: permission denied'
# after
chmod u+w rl.json.sig   # or: rm rl.json.sig
signer extend-revocation-list --key-id 1a2b3c4d5e6f7080 --revocation-list-file rl.json --private-root-key root.pem
signer verify-revocation-list --revocation-list-file rl.json --signature-file rl.json.sig --public-root-key root-public.pem
Defensive patterns

Strategy: validation

Validate before calling

func preflightWrites(paths ...string) error {
    for _, p := range paths {
        if err := ensureWritableDir(p); err != nil {
            return err
        }
        if info, err := os.Stat(p); err == nil {
            if info.Mode().Perm()&0o200 == 0 {
                return fmt.Errorf("%s not writable by uid %d", p, os.Getuid())
            }
        }
    }
    return nil
}

// covers both rlPath and rlPath+".sig" — the second write is the easy one to miss

Prevention

When it happens

Trigger: The <list>.sig path is unwritable (an old .sig owned by another user with 0600 perms is the classic case), the directory went read-only between the two writes, or the disk filled exactly before the second write.

Common situations: A .sig created by root in an earlier run, then extend run as a non-root user; permission hardening applied between runs; ENOSPC on nearly-full volumes.

Related errors


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