karolsluszniak

karolsluszniak

I’m developing a Phoenix server that needs to terminate SSL for multiple domains with certificates acquired via Certbot & stored in database in order to support the custom domains feature in my app. I’m passing sni_fun callback to https keyword list in Phoenix like so:

config :my_app, MyAppWeb.Endpoint,
  # ...
  https: [
    # ...
    sni_fun: &MyAppWeb.Certs.sni_fun/1
  ]

which works perfectly fine as long as it relies on filesystem by passing certfile, keyfile and cacertfile in the sni function like so:

def sni_fun(domain) do
  domain = List.to_string(domain)
  certs_dir = Path.join(:code.priv_dir(:my_app), "cert")
  certfile = Path.join(certs_dir, "#{domain}.pem")
  cacertfile = Path.join(certs_dir, "#{domain}_chain.pem")
  keyfile = Path.join(certs_dir, "#{domain}_key.pem")

  certs = [certfile: certfile, cacertfile: cacertfile, keyfile: keyfile]

  get_and_write_certs(domain, certs)

  certs
end

Notice that certfile, cacertfile and keyfile map to cert.pem, chain.pem and privkey.pem obtained by certbot respectively.

But this means that I have to rely both on database and filesystem to deliver certificates. This bites when trying to figure out a proper & efficient caching strategy considering there’s already a cache in place. I’ve been trying to use equivalent cert, cacerts and key options that appear both in erlang SSL docs and ranch docs, with this code being what I believe is the closest to SSL and ranch specs as well as notes in this thread about HTTPoison:

def sni_fun(domain) do
  domain = List.to_string(domain)
  {cert_pem_string, cacerts_pem_string, key_pem_string} = get_certs(domain)

  cert = read_pem(cert_pem_string) |> hd() |> elem(1)
  cacerts = read_pem(cacerts_pem_string) |> Enum.map(&elem(&1, 1))
  key = read_pem(key_pem_string) |> hd()

  [cert: cert, cacerts: cacerts, key: key]
end

defp read_pem(pem_string) do
  pem_string
  |> :public_key.pem_decode()
  |> Enum.map(fn entry ->
    entry = :public_key.pem_entry_decode(entry)
    type = elem(entry, 0)
    {type, :public_key.der_encode(type, entry)}
  end)
end

This doesn’t work yielding following error in server log:

[info] TLS :server: In state :hello at tls_connection.erl:1359 generated SERVER ALERT: Fatal - Handshake Failure
 - :malformed_handshake_data

Even though when IO.inspecting the values all seems legit and according to above specs:

[
  cert: <<48, 130, 5, 99, 48, 130, 4, 75, 160, 3, 2, 1, 2, 2, 19, 0, 250, 215,
    47, 160, 210, 189, 235, 118, 145, 90, 123, 29, 116, 249, 12, 148, 109, 65,
    48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, ...>>,
  cacerts: [
    <<48, 130, 5, 91, 48, 130, 3, 67, 160, 3, 2, 1, 2, 2, 16, 77, 244, 43, 149,
      209, 238, 155, 58, 76, 46, 179, 59, 141, 16, 93, 214, 48, 13, 6, 9, 42,
      134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 48, ...>>,
    <<48, 130, 5, 84, 48, 130, 4, 60, 160, 3, 2, 1, 2, 2, 17, 0, 237, 93, 91,
      201, 109, 251, 223, 77, 62, 205, 106, 73, 141, 209, 179, 199, 48, 13, 6,
      9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, ...>>
  ],
  key: {:RSAPrivateKey,
   <<48, 130, 4, 163, 2, 1, 0, 2, 130, 1, 1, 0, 172, 232, 46, 48, 27, 142, 154,
     67, 197, 56, 70, 245, 47, 232, 199, 248, 192, 53, 199, 211, 94, 83, 73, 35,
     24, 181, 123, 43, 194, 15, 191, 59, 16, ...>>},
]

Don’t worry, although trimmed it’s not a production certificate :slight_smile:

I’ve also tried other variations e.g. by skipping the pem_entry_decode + der_encode calls (since pem_decode already seems to return valid DER encoded binary, just with extra element in entity tuple). Nothing seems to work unless returning to fs-based implementation…

So has anyone figured out any way to turn certificates (returned by Let’s Encrypt or not) into an in-memory representation that phoenix / cowboy / ranch / erlang ssl would consume properly? Or do you see any mistake that I’m making here? This could be useful for many other use cases like configuring certs in config/runtime.exs from various secret stores without relying on filesystem.

Showing Posts 1 to 5

voltone

voltone

I checked some code of mine in which I’m doing something similar, and in the key parameter I am passing in a PKCS#8 PrivateKeyInfo DER binary. I don’t remember why, I may have had some issues passing in raw RSA keys, or maybe I just decided it was more portable, as it allows me to pass in EC key binaries as well.

Anyway, if you want to try that I would recommend you use x509 (shameless plug) to do the PEM/DER conversion, otherwise it’s a bit tricky to produce the PKCS#8 wrapper. This seems to work for me: key: {:PrivateKeyInfo, X509.PrivateKey.to_der(key, wrap: true)}, where key is the internal Erlang RSA key record (which you can get using X509.PrivateKey.from_pem(pem)). I guess you could store the PKCS#8 DER in your DB so you don’t have to convert it each time…

voltone

voltone

Hmm, I changed it to key: {:RSAPrivateKey, X509.PrivateKey.to_der(key, wrap: false)} and it still works. Looking at the surrounding code it really was just to support different key types with one code path. So that probably won’t make a difference for you.

Which OTP version are you on?

karolsluszniak

karolsluszniak OP

So it turned out that the code above was perfectly valid like it was supposed to be :slight_smile:

After posting here I’ve decided to move along with fs-based way but after getting @voltone ’s hint (thank you!) I’ve again put together the code posted above and it worked. I must’ve made some silly mistake after doing 100 iterations with varying cert payloads or had inconsistency in certs table.

Anyway, it took many hours to put this together so I hope my code helps others. It’s surprising that this is such an undocumented subject considering elixir and phoenix are a perfect match for building scalable servers that terminate ssl for multiple domains. I’ll consider turning this into a library or a blog post (or both).

Again, thanks @voltone for taking your time! And making me do the last lucky shot :slight_smile:

axelson

axelson

Scenic Core Team

I actually do this for a hobby project that is utilizing GitHub - sasa1977/site_encrypt: Integrated certification via Let's encrypt for Elixir-powered sites · GitHub

karolsluszniak

karolsluszniak OP

I now know where my mistake came from. It turns out that any exception raised in sni_fun is muted and instead the :malformed_handshake_data is produced in logs, giving no clue about an error in the callback, which I probably had in the pre-last iteration but fixed by rewriting the code. This is super-confusing and I don’t even know where this error is muted seeing that the SSL termination part of Phoenix endpoint falls somewhere between cowboy, ranch and erlang’s ssl module…

@axelson Yeah, I saw site_encrypt but wanted a database-centered solution, seeing that as easiest & most obvious way to store multiple certs in Phoenix + Ecto + Postgres app. I’m not a SSL expert so please correct me if I’m wrong but seems to me that site_encrypt doesn’t cover this case (i.e. dynamic certfificate issuing without server restarts and with certs instantly distributed among multiple server instances).

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews