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

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New

Other Trending Topics Top

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
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews