Skip to content

Integrated certification via Let's encrypt for Elixir-powered sites

License

Notifications You must be signed in to change notification settings

aforward-oss/site_encrypt

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SiteEncrypt

Build Status

This project aims to provide integrated certification via Let's encrypt for sites implemented in Elixir.

Integrated certification means that you don't need to run any other OS process in background. Start your site for the first time, and it will obtain the certificate, and restart the endpoint. The system will also periodically renew the certificate, and when the new certificate is obtained, the endpoint will again be restarted.

The target projects are small-to-medium Elixir based sites which don't sit behind reverse proxies such as nginx.

In addition, the library ships with a basic ACME v2 server to facilitate local development without needing to start a bunch of docker images.

Status

Extreme alpha. It's highly unstable, unfinished, there's no documentation, no tests, and the API can change radically. Use at your own peril :-)

Dependencies

  • Certbot >= 0.22 (ACME client used to obtain certificate)

I have plans to replace Certbot with a native implementation in Elixir, but can't promise when will that happen, or if it will happen at all.

Using with Phoenix

Local development

A basic demo Phoenix project is available here.

In a nutshell, you need to do the following things:

  • Add the dependency
  • Create the certbot callback module
  • Add ACME challenge plug into the plug pipeline
  • Adapt endpoint configuration
  • Start your endpoint through SiteEncrypt

Let's break this one step at a time.

First, you need to add the dependency to mix.exs:

defmodule PhoenixDemo.Mixfile do
  # ...

  defp deps do
    [
      # ...
      {:site_encrypt, github: "sasa1977/site_encrypt"}
    ]
  end
end

Don't forget to invoke mix.deps after that.

Next, you need to create the module which provides the certbot client configuration:

defmodule PhoenixDemoWeb.Certbot do
  @behaviour SiteEncrypt

  def https_keys(), do: SiteEncrypt.https_keys(config())

  @impl SiteEncrypt
  def config() do
    %{
      run_client?: unquote(Mix.env() != :test),
      ca_url: local_acme_server(),
      domain: "foo.bar",
      extra_domains: ["www.foo.bar", "blog.foo.bar"],
      email: "admin@foo.bar",
      base_folder: Application.app_dir(:phoenix_demo, "priv") |> Path.join("certbot"),
      cert_folder: Application.app_dir(:phoenix_demo, "priv") |> Path.join("cert"),
      renew_interval: :timer.hours(6),
      log_level: :info
    }
  end

  @impl SiteEncrypt
  def handle_new_cert() do
    # Optionally backup the folder configured via`:base_folder`.
    :ok
  end

  defp local_acme_server(), do: {:local_acme_server, %{adapter: Plug.Adapters.Cowboy, port: 4002}}
end

Then, add the ACME challenge plug in your endpoint. I recommend to add it immediately after the logger plug:

defmodule PhoenixDemoWeb.Endpoint do
  # ...

  plug Plug.Logger

  plug SiteEncrypt.AcmeChallenge, PhoenixDemoWeb.Certbot

  # ...
end

In your endpoint you need to configure HTTPS if the cert is present:

defmodule PhoenixDemoWeb.Endpoint do
  # ...

  def init(_key, config) do
    # add HTTPS config
    config = Keyword.put(config, :https, [port: 4001] ++ PhoenixDemoWeb.Certbot.https_keys())

    # ...
  end
end

And finally, you need to start the endpoint via SiteEncrypt:

defmodule PhoenixDemo.Application do
  use Application

  def start(_type, _args) do
    children = [
      SiteEncrypt.Phoenix.child_spec({PhoenixDemoWeb.Certbot, PhoenixDemoWeb.Endpoint})
    ]

    opts = [strategy: :one_for_one, name: PhoenixDemo.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # ...
end

And that's it! At this point you can start the system:

$ iex -S mix phx.server

[info] Generating a temporary self-signed certificate. This certificate will be used until a proper certificate is issued by the CA server.
[info] Running local ACME server at port 4002
[info] Running PhoenixDemoWeb.Endpoint with Cowboy using http://0.0.0.0:4000
[info] Starting certbot
...
[info] Obtained new certificate for foo.bar
[info] Certbot finished

And visit your certified site at https://localhost:4001

The certificate issued by the integrated ACME server expires after one day. Therefore, if you restart the site, it will be renewed.

If something goes wrong, usually if you abruptly took down the system in the middle of the certification, the certbot might not work again. In this case, you can just delete the contents of the certbot folder (as defined in PhoenixDemoWeb.Certbot.folder/1).

Of course, in real production you want to backup this folder after every change, and restore it if something is corrupt.

Production

To make it work in production, you need to own the domain and run your site there.

You need to change some parameters in config/1 in the certbot module:

def config() do
  %{
    ca_url: proper_url,
    domain: your_domain,
    extra_domains: other_domains,
    email: your_email,

    # other parameters can remain the same
  }
end

For ca_url, you can provide https://acme-staging-v02.api.letsencrypt.org/directory (staging) or https://acme-v02.api.letsencrypt.org/directory (production). Provide the complete URL.

I strongly advise to try it with the staging version first. If that works, then you can switch ca_url to production. Before you make that switch, you'll need to remove the certbot folder defined in PhoenixDemoWeb.Certbot.folder/1.

You can also consider trying with a locally running boulder server first. Setting it up requires a couple of manual steps, but it was relatively straightforward. Ping me if you get stuck here.

Once you have your proper cert, make sure to backup the entire contents of the certbot folder. If you're moving to the new machine, you should probably restore the backup to the certbot folder.

Don't use a small renew_interval, because you might trip the rate limit.

It's up to you to decide how to vary the settings between local development and production. Personally, I use OS env vars, so I can run the :prod version which self-certifies on a local machine. The relevant code is available here.

Restoring a backup

restore previous cert and certbot directories. In the certbot directory, there should be symlinks in the subdirectory certbot/config/live/MY_DOMAIN/. There should be 4 symlinks to 4 files (cert.pem, fullchain.pem, privkey.pem and chain.pem). If the symlinks are preserved you have nothing more to do. If they are not.

  • go check into certbot/config/archive/MY_DOMAIN/
  • you should see some files, at least cert1.pem fullchain1.pem, privkey1.pem and chain1.pem. They correspond to the initially issued certificate. If your certificate has been renewed, you will find also cert2.pem, fulllchain2.pem... You'll have to make the symlink between the latest of these files and the live directory. So go to certbot/config/live/
  • type the following (if your certificate has been renewed, replace the 1 with the latest number you have)
rm cert.pem chain.pem fullchain.pem privkey.pem
ln -s ../../archive/MY_DOMAIN/cert1.pem cert.pem
ln -s ../../archive/MY_DOMAIN/chain1.pem chain.pem
ln -s ../../archive/MY_DOMAIN/fullchain1.pem fullchain.pem
ln -s ../../archive/MY_DOMAIN/privkey1.pem privkey.pem

and you're good to go!

Warning

At the moment, I wouldn't advise using this for any critical production, because it's highly untested. I do use it myself to certify my blog, but it's just been used for a few months, and so there are probably many uncovered issues.

License

MIT

About

Integrated certification via Let's encrypt for Elixir-powered sites

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Elixir 90.2%
  • JavaScript 6.5%
  • HTML 3.2%
  • CSS 0.1%