joewalnes/websocketd · critical

failed to parse CA certificates from %s

Error message

failed to parse CA certificates from %s

What it means

The CA file was readable but x509.AppendCertsFromPEM parsed no certificates out of it, so websocketd cannot build a ClientCAs pool and refuses to start mutual TLS with this error. AppendCertsFromPEM only reports false when the data contains zero usable certificates.

Source

Thrown at main.go:112

}

// tlsConfig returns the base TLS settings shared by all HTTPS servers. It pins
// a minimum protocol version explicitly rather than relying on the Go default,
// which has drifted across releases.
func tlsConfig() *tls.Config {
	return &tls.Config{MinVersion: tls.VersionTLS12}
}

// serveMutualTLS runs an HTTPS server on the given listener that requires
// client certificates verified against the given CA file.
func serveMutualTLS(listener net.Listener, certFile, keyFile, caFile string, log *libwebsocketd.LogScope) error {
	caCert, err := os.ReadFile(caFile)
	if err != nil {
		return fmt.Errorf("failed to read CA file %s: %w", caFile, err)
	}
	caCertPool := x509.NewCertPool()
	if !caCertPool.AppendCertsFromPEM(caCert) {
		return fmt.Errorf("failed to parse CA certificates from %s", caFile)
	}

	cfg := tlsConfig()
	cfg.ClientAuth = tls.RequireAndVerifyClientCert
	cfg.ClientCAs = caCertPool
	server := &http.Server{
		ReadHeaderTimeout: readHeaderTimeout,
		TLSConfig:         cfg,
	}
	log.Info("server", "Mutual TLS enabled (client certs verified against %s)", caFile)
	return server.ServeTLS(listener, certFile, keyFile)
}

// unixSocketProbeTimeout bounds the liveness probe against an existing socket
// file. A local AF_UNIX connect either succeeds or is refused immediately; the
// timeout only guards against a pathological listener that accepts nothing.
const unixSocketProbeTimeout = time.Second

View on GitHub (pinned to 7a8683dc7f)

Solutions

  1. Inspect the file: it must contain PEM blocks starting with '-----BEGIN CERTIFICATE-----'; convert DER with `openssl x509 -inform der -in ca.der -out ca.pem`.
  2. Verify content sanity with `openssl x509 -in ca.pem -noout -text` and re-export the file if it's empty or truncated.
  3. Make sure the CA certificate, not a private key or server cert, is configured for --ssl-ca-file.
  4. If chaining, concatenate each CA as PEM blocks with proper newlines between them.

Example fix

// before (DER file passed directly)
--ssl-ca-file=/etc/pki/ca.der
// after
openssl x509 -inform der -in /etc/pki/ca.der -out /etc/pki/ca.pem
--ssl-ca-file=/etc/pki/ca.pem
Defensive patterns

Strategy: validation

Validate before calling

import "crypto/x509", "encoding/pem", "os"
func validPEMCertPool(path string) error {
    data, err := os.ReadFile(path)
    if err != nil { return err }
    pool := x509.NewCertPool()
    if !pool.AppendCertsFromPEM(data) {
        return fmt.Errorf("%s contains no PEM certificates", path)
    }
    return nil
}

Try / catch

if err := startServer(); err != nil {
    if strings.Contains(err.Error(), "failed to parse CA certificates") {
        log.Fatalf("CA bundle invalid — regenerate with: openssl x509 -inform der -in ca.der -out ca.pem")
    }
    return err
}

Prevention

When it happens

Trigger: --ssl-ca-file points at a DER-encoded cert (not PEM), an empty or truncated file, a key/private-key file passed instead of a certificate, concatenated junk/garbage, or a file containing only intermediate certs in an unreadable encoding.

Common situations: Downloading a CA from a portal that serves DER by default; redirecting the wrong variable into the file (echo $KEY > ca.pem); file truncated by a failed secret mount; mixing up --ssl-ca-file (client-verification CA) with --ssl-cert (server cert).

Understand the failure class

Related errors


AI-assisted analysis of joewalnes/websocketd@7a8683dc7f (2026-09-03). Data as JSON: /api/errors/675d8bb0d46a4953. Report an issue: GitHub.