ssl_config.erl treats ‘certfile’ and ‘cacertfile’ in two slightly different ways. I’m not sure why. Either way, it turned into a gotcha for me.
What happened
Yesterday, my site monitoring alerted me to a certificate problem:
curl: (60) SSL certificate problem: unable to get local issuer certificate
Studying the logs, I saw this happened shortly after ‘certbot’ automatically renewed the site’s certificates. I also saw that the problem went away the next time the webserver (yaws) was restarted, for log rotation.
Why are there two certificates?
‘Letsencrypt’, and probably other CAs, use a two-step way of signing certificates involving two certificates:
leaf certificate: “Letsencrypt’s delegated signer says this certificate belongs to Corelatus”
intermediate certificate: “The delegated signer belongs to Letsencrypt”
Letsencrypt could have used their root certificate to sign my leaf
certificate, but they don’t, for practical reasons. The result is that
the browser needs both certificates.
Where does Certbot put certificates?
Certbot puts certificates in /etc/letsencrypt/archive, e.g.
cert15.pem // the leaf
chain15.pem // the intermediate
fullchain15.pem // both in one file
Certbot then symlinks all three of them from /etc/letsencrypt/live/corelatus.com, basically
cert.pem → /etc/letsencrypt/archive/cert15.pem
fullchain.pem → /etc/letsencrypt/archive/fullchain15.pem
What does Yaws do?
Yaws just passes its configuration on to SSL. In my case, that was
certfile: /etc/letsencrypt/live/corelatus.com/cert.pem
cacertfile: /etc/letsencrypt/live/corelatus.com/fullchain.pem
What does Erlang’s SSL code do?
Erlang’s SSL code has a cache for the certificates, i.e. it doesn’t re-read them all the time. You can snoop the cache because it’s in an ETS table:
ets:tab2list(ssl_pem_cache).
[{<<“/etc/letsencrypt/live/corelatus.com/cert.pem”>>,
{<<“/etc/letsencrypt/live/corelatus.com/../../archive/corelatus.com/fullchain15.pem”>>
There’s a difference between the leaf and the full chain: the leaf gets the symlink. The fullchain got resolved to the actual file. That was a bit surprising. It turns out that the ssl_config.erl treats the two options differently. Only "opt_cacerts() calls unambiguous_path() to change the symlink into a link-free filename.
The result is that when ‘certbot’ updates the certificate, Erlang’s SSL code picks up the new leaf, but not the new intermediate.
How did I fix this?
I fixed it by changing my ‘yaws’ configuration:
old: certfile=/etc/letsencrypt/live/corelatus.com/cert.pem cacertfile=/etc/letsencrypt/live/corelatus.com/fullchain.pem
fixed: certfile=/etc/letsencrypt/live/corelatus.com/fullchain.pem
Another possible fix would have been to add a post-deployment hook to ‘certbot’, i.e. automatically restart yaws after certbot updates certificates.
Why am I posting this to the forum?
Mainly because I would have liked to have stumbled across this explanation before I started ![]()
But also a bit because someone might know something I don’t. I’m left wondering
- If you can put the full chain in ‘certfile’, why do we need the ‘cacertfile’ option? Why not just always put them in the same file? (Historical? Maybe some types of certificates can’t be combined?)
- Why is unambiguous_path() called at all? What’s the benefit in eliminating symlinks?
Matthias