Billionmail/BillionMail · error

Failed to save certificate file: {}

Error message

Failed to save certificate file: {}

What it means

Thrown in ApplySSLWithExistingServer when public.WriteFile fails to persist certificate.pem into savePath after a successful issuance. The message embeds the underlying error, typically an I/O permission problem or the directory not existing despite the earlier MkdirAll (e.g. removed concurrently or race with another process).

Source

Thrown at core/internal/service/acme/acme.go:484

	}

	// Save certificate files if path is provided
	if savePath != "" {
		// Create directory if it doesn't exist
		if !public.FileExists(savePath) {
			err = os.MkdirAll(savePath, 0750)
			if err != nil {
				return "", "", errors.New(public.LangCtx(ctx, "Failed to create directory: {}", err.Error()))
			}
		}

		// Save certificate and private key files
		certificateFile := filepath.Join(savePath, "certificate.pem")
		privateKeyFile := filepath.Join(savePath, "private_key.pem")

		_, err = public.WriteFile(certificateFile, string(certificates.Certificate))
		if err != nil {
			return "", "", errors.New(public.LangCtx(ctx, "Failed to save certificate file: {}", err.Error()))
		}

		_, err = public.WriteFile(privateKeyFile, string(certificates.PrivateKey))
		if err != nil {
			return "", "", errors.New(public.LangCtx(ctx, "Failed to save private key file: {}", err.Error()))
		}
	}

	// Return certificate
	return string(certificates.Certificate), string(certificates.PrivateKey), nil
}

type CertInfo v1.CertInfo

/**
 * @description: Get certificate information
 * @param {string} certificateStr Certificate string
 * @return {CertInfo} Certificate information

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Read the embedded OS error: fix permission denied / no space left on device accordingly
  2. Ensure the process user owns savePath (chown app-user:app-user <savePath>) or run with sufficient privileges
  3. Check disk space (df -h) and inode availability on the target volume
  4. Confirm no external process (systemd tmpfiles, config sync) deletes/recreates the directory during issuance

Example fix

// before
svc.ApplySSLWithExistingServer(ctx, d, "rsa2048", cert, key, "/root/certs") // app runs as non-root
// after
svc.ApplySSLWithExistingServer(ctx, d, "rsa2048", cert, key, "/var/lib/billionmail/certs") // chown app-user
Defensive patterns

Strategy: validation

Validate before calling

if err := os.MkdirAll(savePath, 0750); err != nil { return err }
probe := filepath.Join(savePath, ".write_probe")
if err := os.WriteFile(probe, []byte("ok"), 0600); err != nil { return err }
os.Remove(probe)
if st, err := os.Statfs(savePath); err == nil && st.Bavail == 0 { return errors.New("no space on target volume") }

Try / catch

if _, _, err := svc.ApplySSLWithExistingServer(ctx, d, kt, c, k, savePath); err != nil {
    var pe *fs.PathError
    if errors.As(err, &pe) && pe.Op == "open" {
        log.Printf("cannot write cert to %s: %v", savePath, pe.Err)
    }
    return err
}

Prevention

When it happens

Trigger: ApplySSLWithExistingServer reaches public.WriteFile(certificateFile, string(certificates.Certificate)) and the write fails — savePath was deleted between MkdirAll and write, disk full, or permissions deny the process from creating the file.

Common situations: Disk quota/full volume on busy mail servers; directory recreated by config management with different ownership between mkdir and write; SELinux/AppArmor blocking writes to the cert directory; running service as non-root while path is root-owned.

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/be363ef04dc5726e. Report an issue: GitHub.