Billionmail/BillionMail · error

failed to write key file: %v

Error message

failed to write key file: %v

What it means

Same write path as the certificate error but for the private key file <SSL_PATH>/postfix.key. updatePostfixConfig wraps os.WriteFile failure for the TLS private key with the OS error embedded. Without this key Postfix cannot complete the TLS handshake, so SSL setup aborts.

Source

Thrown at core/internal/service/mail_service/certificate.go:244

// updatePostfixConfig updates Postfix configuration with new certificate
func (c *Certificate) updatePostfixConfig(csrPem, keyPem string) error {
	mainCf := public.AbsPath(consts.POSTFIX_MAIN_CONF)
	content, err := os.ReadFile(mainCf)
	if err != nil {
		return fmt.Errorf("failed to read postfix config: %v", err)
	}

	// Write certificate and key to files
	certPath := public.AbsPath(filepath.Join(consts.SSL_PATH, "postfix.crt"))
	keyPath := public.AbsPath(filepath.Join(consts.SSL_PATH, "postfix.key"))

	if err := os.WriteFile(certPath, []byte(csrPem), 0755); err != nil {
		return fmt.Errorf("failed to write certificate file: %v", err)
	}

	if err := os.WriteFile(keyPath, []byte(keyPem), 0755); err != nil {
		return fmt.Errorf("failed to write key file: %v", err)
	}

	// Update SSL certificate configuration
	config := string(content)
	config = c.updateConfigLine(config, "smtpd_tls_key_file", keyPath)
	config = c.updateConfigLine(config, "smtpd_tls_cert_file", certPath)

	if err := os.WriteFile(mainCf, []byte(config), 0755); err != nil {
		return fmt.Errorf("failed to write postfix config: %v", err)
	}

	return nil
}

// SetPostfixVMailCert configures SSL certificate for Postfix virtual mail
func (c *Certificate) SetPostfixVMailCert(domain, csrPem, keyPem string) error {
	// Validate certificate data
	if err := c.verifyCertificate(csrPem, keyPem); err != nil {

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Verify SSL_PATH exists and the process user can write to it (ls -ld, chown/chmod as needed).
  2. Check disk space and Docker volume mounts for the SSL path.
  3. Remove/rename any directory incorrectly sitting at the postfix.key path.
  4. Read the embedded OS error to distinguish permission vs space vs read-only causes.
  5. Re-run SetSSL and confirm postfix.key exists with correct contents.

Example fix

// before
if err := os.WriteFile(keyPath, []byte(keyPem), 0755); err != nil {
    return fmt.Errorf("failed to write key file: %v", err)
}
// after
if err := os.WriteFile(keyPath, []byte(keyPem), 0600); err != nil {
    return fmt.Errorf("failed to write key file %s: %w", keyPath, err)
}
Defensive patterns

Strategy: validation

Validate before calling

keyPath := public.AbsPath(filepath.Join(consts.SSL_PATH, "postfix.key"))
if st, err := os.Stat(filepath.Dir(keyPath)); err != nil || !st.IsDir() {
    return fmt.Errorf("ssl dir missing")
}
if err := unix.Access(filepath.Dir(keyPath), unix.W_OK); err != nil {
    return fmt.Errorf("ssl dir not writable: %v", err)
}

Try / catch

err := svc.SetSSL(ctx, domain, certPem, keyPem)
if err != nil && strings.Contains(err.Error(), "failed to write key file") {
    log.Printf("verify SSL dir ownership and free space before retrying: %v", err)
}

Prevention

When it happens

Trigger: SetSSL or SetPostfixSSL invoked when SSL_PATH is missing/unwritable, the key write hits ENOSPC/EACCES/EROFS, or the key file path collides with a directory.

Common situations: Read-only or unmounted SSL volume in Docker; key file previously created by root and now written by a lower-privileged process; disk full; path exists as a directory.

Understand the failure class

Background: "Permission denied" / "Failed to write" file errors: why a library can't write its files to disk (EACCES, EPERM, ENOSPC) and how to fix them — this error's family across 43 libraries.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/78e3d93772cf984f. Report an issue: GitHub.