What you're seeing
$ openssl s_client -connect sip.example.com:5061 -servername sip.example.com
...
Verify return code: 21 (unable to verify the first certificate)In Kamailio's log, the tls module errors include the OpenSSL reason, for example no shared cipher, wrong version number, sslv3 alert handshake failure, tlsv1 alert unknown ca or certificate verify failed. The reason tells you which side is unhappy.
Likely causes
Most common first.
- Incomplete certificate chain. Browsers fetch missing intermediates; SIP clients (Teams, Yealink, Polycom, mobile SDKs) usually don't. If Kamailio serves only the leaf certificate, verification fails. The certificate file must be the full chain.
- Protocol or cipher mismatch. The two sides don't share a TLS version or cipher. Old handsets may only speak TLS 1.0/1.1, which modern OpenSSL builds refuse by default. 'wrong version number' also appears when one side speaks plain TCP to a TLS port.
- Client-certificate verification on the server profile. With require_certificate enabled on the server profile, clients must present a certificate signed by a CA in ca_list. Phones usually don't have one. Carriers and Microsoft Teams Direct Routing do, and need their CA trusted.
- A renewed certificate Kamailio never loaded. Kamailio reads certificate files at startup or on tls.reload. After a Let's Encrypt renewal it keeps serving the old one until reloaded. If the new files aren't readable by the kamailio user, reload fails.
- Wrong certificate for the name (SNI). If clients connect to a name the certificate doesn't cover, verification fails. Use a certificate that covers every name, or per-domain server profiles in tls.cfg selected by server_name.
How to fix
1. Minimal working TLS setup
enable_tls=yes
listen=tls:203.0.113.10:5061
loadmodule "tls.so"
modparam("tls", "config", "/etc/kamailio/tls.cfg")[server:default]
method = TLSv1.2+
verify_certificate = no
require_certificate = no
private_key = /etc/kamailio/tls/privkey.pem
certificate = /etc/kamailio/tls/fullchain.pem
[client:default]
method = TLSv1.2+
verify_certificate = yes
require_certificate = yes
ca_list = /etc/ssl/certs/ca-certificates.crtcertificate must be the full chain (leaf then intermediates), not cert.pem. For Teams Direct Routing or carriers doing mutual TLS, set verify_certificate = yes, require_certificate = yes and a ca_list on the server side too.
2. Reload certificates after renewal
#!/bin/sh
set -e
install -o root -g kamailio -m 0640 "$RENEWED_LINEAGE/fullchain.pem" /etc/kamailio/tls/fullchain.pem
install -o root -g kamailio -m 0640 "$RENEWED_LINEAGE/privkey.pem" /etc/kamailio/tls/privkey.pem
kamcmd tls.reloadCopying into /etc/kamailio/tls/ avoids the permissions on /etc/letsencrypt/live, which the kamailio user can't read. tls.reload only affects new connections, so existing TLS connections keep the old certificate until they reconnect.
3. Verify from the client's point of view
# chain and expiry as a client sees them
openssl s_client -connect sip.example.com:5061 -servername sip.example.com -showcerts </dev/null
# does the server accept TLS 1.2?
openssl s_client -connect sip.example.com:5061 -tls1_2 </dev/null
# TLS settings Kamailio has loaded, and current TLS connections
kamcmd tls.options
kamcmd tls.listVerify return code: 0 (ok) against the system CA store is what a phone needs to see.