amir20/dozzle · error
failed to parse certificate
Error message
failed to parse certificate: %w
What it means
NewClient fails with "failed to parse certificate: %w" when x509.ParseCertificate cannot decode the DER bytes of the leaf certificate loaded from shared_cert.pem. This means the certificate file exists but its contents are corrupt, truncated, or not a valid X.509 certificate.
Solutions
- Regenerate the shared certificates with `make generate` on the host and restart both agent and server so both sides use the same keypair
- Verify shared_cert.pem is a valid PEM certificate: `openssl x509 -in shared_cert.pem -text -noout`; replace if it errors
- Ensure the cert volume/mount is complete and not truncated (check file size and that the container sees the updated file)
- Check for version skew: upgrade client and agent to the same Dozzle release
Example fix
// before
certPEM, _ := os.ReadFile("stale-shared_cert.pem")
certificates, err := tls.X509KeyPair(certPEM, keyPEM) // leaf may be corrupt
client, err := agent.NewClient(endpoint, certificates)
// after
certPEM, err := os.ReadFile("shared_cert.pem") // regenerated via make generate
if err != nil { return err }
if _, err := tls.X509KeyPair(certPEM, keyPEM); err != nil {
return fmt.Errorf("invalid shared cert, regenerate with make generate: %w", err)
}
client, err := agent.NewClient(endpoint, certificates) Defensive patterns
Strategy: validation
Validate before calling
certPEM, err := os.ReadFile("shared_cert.pem")
if err != nil { return err }
if _, err := tls.X509KeyPair(certPEM, keyPEM); err != nil {
return fmt.Errorf("corrupt shared cert; run make generate: %w", err)
} Try / catch
client, err := agent.NewClient(endpoint, certs)
if err != nil {
if strings.Contains(err.Error(), "failed to parse certificate") {
log.Fatal().Err(err).Msg("regenerate shared certs with make generate")
}
return err
} Prevention
- Regenerate and redistribute shared_cert.pem/shared_key.pem atomically on both server and agent after `make generate`
- Verify certs with `openssl x509 -in shared_cert.pem -text -noout` before deploying
- Mount the cert files read-only and ensure volume contents are fully synced before starting clients
When it happens
Trigger: agent.NewClient(endpoint, certificates) is called and certificates.Certificate[0] (the leaf DER bytes from the shared cert file) cannot be parsed by crypto/x509.
Common situations: shared_cert.pem regenerated or corrupted mid-deploy so clients hold a stale/mismatched copy; file mounted empty or with placeholder text; an old client image using certs from a newer algorithm; copying the PEM text file where DER bytes were expected.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to parse certificate
- failed to connect to
- EOF error while converting gRPC to error
- unknown error: with
- unknown code: with
AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07).
Data as JSON: /api/errors/53e569cc38f326d2.
Report an issue: GitHub.
Appendix: source
Thrown at internal/agent/client.go:49
type Client struct {
client pb.AgentServiceClient
conn *grpc.ClientConn
endpoint string
nameOverride string
group string
}
func NewClient(endpoint string, certificates tls.Certificate, opts ...grpc.DialOption) (*Client, error) {
endpoint, nameOverride, group, err := ParseEndpoint(endpoint)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
c, err := x509.ParseCertificate(certificates.Certificate[0])
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
caCertPool.AddCert(c)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{certificates},
RootCAs: caCertPool,
InsecureSkipVerify: true, // Set to true if the server's hostname does not match the certificate
}
// Create the gRPC transport credentials
creds := credentials.NewTLS(tlsConfig)
opts = append(opts,
grpc.WithTransportCredentials(creds),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(10*1024*1024), grpc.UseCompressor(gzip.Name)),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second,
Timeout: 10 * time.Second,
PermitWithoutStream: true,View on GitHub (pinned to d9463cbe21)