CertMagex - Automatic SSL certs from Let's Encrypt for your Phoenix app

Hey there,

you’re getting tired of setting up your SSL private keys manually? Worry no longer CertMagex allows you to get automatic certificates for your domain from Let’s Encrypt.

For Cowboy (standard before) add this to your prod.exs:

config <your_app>, <your_endpoint>,
  https: [port: 443, sni_fun: &CertMagex.sni_fun/1],
  # ATTENTION: Ensure you comment http: out and port 80 is free!
  ...

For Bandit (standard in newer Phoenix templates) add this to your prod.exs:

config <your_app>, <your_endpoint>,
  https: [port: 443, thousand_island_options: [transport_options: [sni_fun: &CertMagex.sni_fun/1]]],
  # ATTENTION: Ensure you comment http: out and port 80 is free!
  ...

And add this to your deps:

def deps do
  [
    {:certmagex, "~> 1.0.0"}
  ]
end

You’re done!

The let’s encrypt handshake is done by the zerossl library GitHub - riccardomanfrin/zerossl: Acme V2 protocol for ZeroSSL library. This is a very early release to get feedback. There are a couple of improvements I would like to see myself:

  • Support other TLS ACME handshakes and not only http on port 80
  • Setup of a fallback certificate / URL if no sni name is specified
  • Let users configure allowlists/blacklists of domain names that should be responded to
  • Better error handling, error debouncing and reporting
  • Support lower Elixir versions (zerossl dependency only supports Elixir v1.15+)

With all that said, please try CertMagex and give me some feedback
Cheers!

18 Likes

Quick question: is it possible to combine this with a nginx reverse proxy config?

Would be neat with postgres ssl as well. I got a repo where I manually set it up with let’s encrypt.

I’m not sure - I guess nginx is in your case terminating TLS already and so Elixir never sees the TLS connection. I think you have to make something nginx aware. Is not using nginx an option?

Note: To allow a non-root Elixir instance to use the ports 443 and 80 you can use setcap on your beam.smp like this:

sudo setcap CAP_NET_BIND_SERVICE=+eip $(elixir -e 'IO.puts(System.find_executable("beam.smp"))')

Cheers!

1 Like

That should be possible. The library works on socket level and just needs you to pass in the sni_fun into the SSL socket server options Erlang -- ssl

So if you in your postgress listener can get access to the socket and inject that option you’re good!

Note though that the acmev2 http handshake two requirements here:

  • The domain you’re requesting a certificate for must point to the IP on which CertMagex is running
  • Port 80 must be free and cannot be in use

So you don’t need to configure postgres to use SSL?

CertMagex is an Elixir library plugging into the beams ssl implementation for it’s magic. But PostgreSQL is written in C. So if you want to use CertMagex with PostgreSQL “server side” then one way of doing so would be to have a tiny Elixir service on the same VM/machine/pod as your postgres instance and that service would proxy the local PostgreSQL and listen on an SSL port using CertMagex.

Extending this idea if you would use Supabase Supavisor GitHub - supabase/supavisor: A cloud-native, multi-tenant Postgres connection pooler. - they could integrate CertMagex and then offer automatic SSL encryption. That is actually a fantastic idea for a PR - will check that out later…

1 Like

Not sure that is relevant, a certificate is a certificate. The other question would be why you would be interested in using a certificate signed by letsencrypt that are exclusively used for web but that is another question in itself.

Self-signed or corporation signed certificates present a danger in itself, however using letsencrypt to sign certificates for postgres is kind of a abuse IMO.

1 Like

What do you use instead?

I never expose postgres to the web. Ideally, if you have custom hardware, you keep them in the same LAN and expose only your service, or if you deploy in the cloud I keep them in the same virtual network.

Back in the day there would be a warning on postgres site about exposing the engine to the web, maybe nowadays it is much more secure, however I would not risk it, as in most cases the data stored in there is critical for business.

1 Like

A lot of people want encryption on everything nowadays but I am also of the opinion that if an attacker penetrates your virtual network then you are more or less toast. Security in depth, I get that and all but IMO the much more CPU cycles spent on this security that would save your bum 1 out of 1_000_000_000 times is to me not worth it.

Though it has to be said that nowadays projects rarely have all their components on the same virtual network so encryption between e.g. Amazon RDS and your Digital Ocean droplet makes sense. But if you are invested in the same cloud then it absolutely does not.

I think you are moving away from the topic, which is about certmagex, which can generate automatic SSL cert from Let’s Encrypt. Furthermore, I want to emphasize that there is absolutely no issue in using a certificate from Let’s Encrypt for securing transport regardless of where it originates from whatsoever.

Even thought you have some great general advise let’s try to keep this on topic.

1 Like

Hey @dominicletz

I am trying to use this on a new phoenix app with Bandit.

I added the package to mix.exs and config/prod.exs, first request to a page fails because the Acmev2 module is missing…
I resolved this by adding included_applications: [:zerossl] to my mix config.
But now it fails with

Mar 16 15:49:02 kinecorman kine_corman[21881]: 15:49:02.554 [error] GenServer CertMagex.Worker terminating
Mar 16 15:49:02 kinecorman kine_corman[21881]: ** (MatchError) no match of right hand side value: {:error, :inets_not_started}
Mar 16 15:49:02 kinecorman kine_corman[21881]:     (zerossl 1.0.0) lib/acmev2.ex:533: Acmev2.serve/1
Mar 16 15:49:02 kinecorman kine_corman[21881]:     (zerossl 1.0.0) lib/acmev2.ex:757: Acmev2.do_gen_cert/2
Mar 16 15:49:02 kinecorman kine_corman[21881]:     (certmagex 1.0.0) lib/certmagex/worker.ex:25: CertMagex.Worker.handle_call/3

Am I missing something?

Solved it by adding :inets to :extra_applications.

Then ran into an issue with zerossl, my machine only has a public IPv6 address, zerossl defaults to "0.0.0.0", I could give "::0", but under the hood, it starts :httpd using :inets.start/2 and that then needs the :ipfamily to be set to :inet6.

A PR for zerossl is awaiting to be merged, using my fork and branch works! :grinning:

1 Like

Thanks @Hermanverschooten I’ve merged those improvements into v1.0.2 Added :inets to extra applications and :zerossl to :included_applicat… · dominicletz/certmagex@7aeba8b · GitHub

:grinning:

Just a small question, you suggest removing the port 80 config from phoenix, probably so it doesn’t interfere with the http server setup by zerosslAcmev2. But how do you redirect visitors to port 80 to https 443?

I’m wondering if a plug would be the more appropriate method of catching acme requests over a whole additional webserver.

I think that may be more what @sasajuric is doing with site_encrypt

I got it working, but it is dirty.

I first removed force_ssl and added a plug (that I found in the past):

defmodule PeppolWeb.Plugs.SSL do
  @moduledoc """
  SSL redirect excluding the health endpoints

  https://github.com/elixir-plug/plug/issues/815
  """

  defdelegate init(opts), to: Plug.SSL

  def call(%{request_path: "/.well-known/" <> _} = conn, _opts), do: conn

  if Application.compile_env(:peppol, :hsts, false) do
    def call(conn, opts) do
      Plug.SSL.call(conn, opts)
    end
  else
    def call(conn, _opts), do: conn
  end
end

Then I looked at where zerossl stores the certificates on my server, this appears to be in /.well-known, I added the following to my endpoint.ex:

  plug Plug.Static,
    at: "/.well-known",
    from: "/.well-known",
    gzip: false

And I set zerossl to listen on port 81 on localhost, so it does not conflict.

config :zerossl, addr: "::1", port: 81

and

config :peppol, hsts: true

When a http requests comes in it is redirected to https by the plug, unless it is for .well-known that is server by Plug.Static, when the https gets it’s first request the Acmev2 will do it’s thing and store the certificate where Plug.Static can serve it.

Told you, dirty…

I forgot to mention that I added PeppolWeb.Plugs.SSL to my endpoint just above the router.

1 Like