joewalnes/websocketd · critical

failed to read CA file %s: %w

Error message

failed to read CA file %s: %w

What it means

When --ssl --ssl-ca-file=FILE is given, websocketd enables mutual TLS: it reads the CA bundle to build the x509 pool that verifies client certificates. This error wraps os.ReadFile's failure — the file could not be opened or read — and aborts startup.

Source

Thrown at main.go:108

		return serveMutualTLS(listener, config.CertFile, config.KeyFile, config.SslCaFile, log)
	}
	server := &http.Server{ReadHeaderTimeout: readHeaderTimeout, TLSConfig: tlsConfig()}
	return server.ServeTLS(listener, config.CertFile, config.KeyFile)
}

// 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

View on GitHub (pinned to 7a8683dc7f)

Solutions

  1. Verify the path in --ssl-ca-file exists from the server's perspective: ls -l <path> as the same user websocketd runs as.
  2. Fix file permissions (chmod/chown) or the volume mount so the process can read the CA file.
  3. Use an absolute path so it resolves regardless of the daemon's working directory.
  4. Confirm the secret/configmap containing the CA is actually deployed and mounted in containerized setups.

Example fix

# before
websocketd --ssl --ssl-cert=cert.pem --ssl-key=key.pem --ssl-ca-file=./ca.pem ./handler
# after (absolute path, readable by the service user)
websocketd --ssl --ssl-cert=cert.pem --ssl-key=key.pem --ssl-ca-file=/etc/websocketd/ca.pem ./handler
Defensive patterns

Strategy: validation

Validate before calling

import "os"
func assertReadableCA(path string) error {
    fi, err := os.Stat(path)
    if err != nil { return fmt.Errorf("CA file %s: %w", path, err) }
    f, err := os.Open(path)
    if err != nil { return fmt.Errorf("CA file %s not readable: %w", path, err) }
    defer f.Close()
    _ = fi
    return nil
}

Try / catch

if err := startServer(); err != nil {
    var pe *fs.PathError
    if errors.As(err, &pe) && strings.Contains(err.Error(), "failed to read CA file") {
        log.Fatalf("CA file %s unreadable: %v", pe.Path, pe.Err)
    }
    return err
}

Prevention

When it happens

Trigger: os.ReadFile(caFile) fails: the --ssl-ca-file path doesn't exist, has a typo, points into a container without the file mounted, or the service user lacks read permission on it.

Common situations: Docker/Kubernetes volume not mounted at the configured path; file created by root with 0600 while the daemon drops privileges; relative path resolved from a different working directory under systemd; secret name typo in deployment manifests.

Related errors


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