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 socketView on GitHub (pinned to 7a8683dc7f)
Solutions
- Verify the path in --ssl-ca-file exists from the server's perspective: ls -l <path> as the same user websocketd runs as.
- Fix file permissions (chmod/chown) or the volume mount so the process can read the CA file.
- Use an absolute path so it resolves regardless of the daemon's working directory.
- 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
- Use absolute paths for --ssl-ca-file.
- Mount CA secrets in containers and verify with `ls -l` as the daemon user before start.
- Set file mode 0644 (readable) on CA bundles; they are public material.
- Validate the path exists in deployment scripts/health checks before launch.
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
- failed to parse CA certificates from %s
- please specify both --sslcert and --sslkey when requesting -
- you should not be using --ssl* flags when there is no --ssl
- ambiguous: provided COMMAND and --dir argument, please only
- did you mean to specify COMMAND instead of --dir '%s'?
AI-assisted analysis of joewalnes/websocketd@7a8683dc7f (2026-09-03).
Data as JSON: /api/errors/2365d72858e68f2b.
Report an issue: GitHub.