The Crystal Programming Language Forum

The fairy tale of one static binary for all Linuxes crashed on the SSL

Hello! Shortly: static binary compiled in the Alpine Linux (as recommended) with libssl (for HTTPS support) is not compatible with all Linuxes, because every Linux distro has different default CA cert paths than Alpine linux (and to each other) :(

Is there any solution for this (some ENV vars? runtime overrides or something?)? Thanks!

3 Likes

Of course, the internet has the answer: Never use static linking! :slight_smile:
https://akkadia.org/drepper/no_static_linking.html

1 Like

Well and other Internet says link statically as much as possible :man_shrugging:

But yeah, with libssl: don’t.

1 Like

This is fascinating. I’ve heard a lot of “don’t statically compile”, as well, but never heard anything more specific than “it causes problems”. I recently wrote a Kubernetes client which I use to write Kubernetes operators in Crystal and I do statically compile them to keep my container images at 5-6MB.

The K8s API does require TLS, so if the issue is the location of the CA certs, apparently the only reason it works is because you specify your own CA cert — they’re self-signed on-the-fly so you have to explicitly trust the cert provided to the pod. This makes a lot of sense and I really appreciate you both providing this context.

Sounds like if I wanted to make calls to third-party services (for example, to provision an AWS resource using a K8s operator), I’d have to compile that one dynamically?

2 Likes

You can still compile statically, but directly for the target platform+distribution (if you statically compile on the linux distribution A, you may have problems with running it on the distribution B).

2 Likes

You can specify path and name of the certs file via the SSL_CERT_FILE environment variable. I statically compile my server on Alpine and then copy and run on the server with: SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt ....

3 Likes

I’m learning all kinds of things in this thread.

1 Like

A tool I maintain had this exact problem, because it’s designed to be compiled statically and used on a fresh install of which there may or may not be useful libraries.

I’ve solved this by overriding OpenSSL::SSL::Context::Client#ca_certificates (see here) when instantiating OpenSSL::SSL::Context::Client.new:

tls_client = OpenSSL::SSL::Context::Client.new.tap do |client|
  client.ca_certificates = "/path/to/cacert.pem"
end

I then point it towards a path of a downloaded cURL/Mozilla CA cert bundle (from curl - Extract CA Certs from Mozilla). You could do something similar by either shipping the CA cert bundle with your program or by shelling out to curl or wget or whatnot when the program launches to fetch the latest bundle if none exists (note the guidance on that page about not fetching it too frequently).

Then when you use HTTP::Client, you just need to pass in the OpenSSL::SSL::Context::Client instance:

HTTP::Client.get("https://example.com/robots.txt", tls: tls_client) do |response|
  # Do stuff
end
4 Likes