WebRTC TURNS Connection Failures? Why Let’s Encrypt Breaks Native Clients (and How to Fix It with ZeroSSL or Google Trust Services)

If you are running a WebRTC infrastructure setup (using platforms like LiveKit or custom STUN/TURN clusters) combined with modern reverse proxies like Caddy or Nginx, you might have recently hit a brick wall.

Everything works beautifully in a standard web browser like Chrome or Firefox, but the moment you test your native mobile application (such as a flutter_webrtc app on Android or iOS), your TURN over TLS (TURNS) connections fail instantly with this cryptic handshake error:

remote error: tls: unknown certificate authority

Here is a deep dive into why this happens, how Let’s Encrypt’s “Generation Y” rollout triggered it, and how you can resolve it server-side without patching your client applications.

The Root Cause: Hardcoded Trust in ssl_roots.h

When a desktop browser connects to a TURNS server, it uses the underlying operating system’s trust store to validate the SSL/TLS certificate. Native WebRTC frameworks do things differently.
To keep the WebRTC engine lightweight and platform-agnostic, the native C++ layer uses an internal OpenSSL mapping. Instead of querying the OS for root certificates, it relies on a statically hardcoded list of trusted root certificate fingerprints compiled directly into the binary at rtc_base/ssl_roots.h. (You can track the ongoing discussions regarding this strict limitation in WebRTC Issue #42221823).
Because this list is a snapshot frozen at compile time, it cannot dynamically adapt to real-world certificate transitions.

Enter Let’s Encrypt’s Generation-Y Rollout

This exact limitation became a major issue when Let’s Encrypt phased out its legacy intermediate paths. All newly requested Let’s Encrypt certificates are now issued via their newer “Generation Y” hierarchy (Root YR and Root YE).
Because these new roots were activated long after your mobile app’s WebRTC engine plugin was built, the native client simply does not recognize them. The client fails to build a trusted validation path, treats your perfectly valid certificate as self-signed, and drops the connection.

Solution 1: Swapping to ZeroSSL

The easiest server-side workaround is to shift your CA provider to ZeroSSL. ZeroSSL utilizes a legacy-compatible root hierarchy anchored under older Sectigo / AAA Certificate Services roots.
Because these roots have been universally trusted for over a decade, their fingerprints are natively baked into virtually every version of ssl_roots.h. This applies to both RSA and ECDSA keys, meaning you do not have to sacrifice connection performance to gain compatibility.

Solution 2: Embracing Google Public CA (Google Trust Services)

If you prefer to leverage Google’s infrastructure, Google Public CA is an exceptional alternative. Google’s own roots are thoroughly represented inside the native WebRTC engine, providing maximum long-term stability for native clients.
Unlike Let’s Encrypt, using Google Public CA requires a few prerequisites before you can request certificates:
  1. An active Google Account.
  2. A registered Google Cloud Project with billing enabled.
  3. An External Account Binding (EAB) secret to link your proxy requests directly to that specific Google Cloud project identity.

Leave a Reply

Your email address will not be published. Required fields are marked *