Install LetsEncrypt On Subdomain With Phoenix / Apache2

Greetings,

I finally got Phoenix working on a sub-domain of my website. However, I still need to get an SSL certificate set up, and I’m getting weird errors. Here is my Apache configuration so far for the sub-domain:

<VirtualHost *:80>
    ServerAdmin admin@mywebsite.com
    ServerName ex.mywebsite.com
    RewriteEngine on
    RewriteCond %{SERVER_NAME} =ex.mywebsite.com
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

<VirtualHost *:443>
    ServerAdmin admin@mywebsite.com
    ServerName ex.mywebsite.com
    DocumentRoot /home/username/mywebsite.com
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    RewriteEngine on
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://127.0.0.1:4000/$1" [P,L]

    ProxyRequests Off
    ProxyPass        / http://127.0.0.1:4000/
    ProxyPassReverse / http://127.0.0.1:4000/

    ProxyPass        /socket/ ws://127.0.0.1:4000/socket/
    ProxyPassReverse /socket/ ws://127.0.0.1:4000/socket/
</VirtualHost>

Here is the command I run to try installing LetsEncrypt:

sudo certbot --http-01-port 4000 --authenticator webroot --webroot-path /home/username/mywebsite.com --installer apache -n -d ex.mywebsite.com

It results in this error:

   Domain: ex.mywebsite.com
   Type:   unauthorized
   Detail: Invalid response from
   https://ex.mywebsite.com/.well-known/acme-challenge/n6pcuMgsQhBtpHd2reDEZpI57fZnVoPo1JbaUNopmtY
   [***.***.***.***]: "<!DOCTYPE html>\n<html>\n<head>\n    <meta
   charset=\"utf-8\">\n    <title>Phoenix.Router.NoRouteError at GET
   /.well-known/acme-challen"

What should I do to get it to install correctly?

Also, for no particular reason, I’m using the same document root “/home/username/mywebsite.com” that I use for my main website. What is the standard practice for the location of the document root? Should I be using something like this instead: “/home/username/HelloWeb”? With HelloWeb being the folder where the my Phoenix project was installed?

Ultimately, I’ll probably just be loading up the Phoenix JavaScript as an external file “https://ex.mywebsite.com/js/app.js” through my main website “https://www.mywebsite.com” and developing my own front-end designs through the main website.

Thanks

The error is because you are forwarding everything to phoenix, including the cerbot’s challenge, which is a static file generated on the fly by certbot. The easiest thing to do is to get the certbot working before setting up any reverse proxy. You do not nee the challenge response to renew the cert, only for the initial setup.

1 Like

Thanks a lot, I greatly appreciate it. This did the trick.

Since it’s almost 90 days, LetsEncrypt is giving me errors that the auto renew is not working. I need some help getting LetsEncrypt installed correctly. As I’ve learned more about Phoenix, there have been some changes and I can’t remember how I got LetsEncrypt installed the first time.

Here is my setup for Apache:

<VirtualHost *:443>
    ServerName ex.mysite.com
    DocumentRoot /home/username/mysite.com
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
    <Location /*>  # is /* path correct for my proxy pass setup?
      Require ip 127.0.0.1 ***.***.***.***
    </Location>
    RewriteEngine on
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://127.0.0.1:4000/$1" [P,L]

    ProxyRequests Off
    ProxyPass        / http://127.0.0.1:4000/
    ProxyPassReverse / http://127.0.0.1:4000/

    ProxyPass        /socket/ ws://127.0.0.1:4000/socket/
    ProxyPassReverse /socket/ ws://127.0.0.1:4000/socket/
</VirtualHost>

Here is the LetsEncrypt certbot command I’m trying to use:

 sudo certbot --authenticator webroot --webroot-path / --installer apache -n -d ex.mysite.com

To begin with, I’m not sure if I’m using the <Location /*> correctly in Apache and how that translates to the certbot command for installing a certificate. The phoenix project works this way, but I’m not sure if it’s the proper way or not. On regular non-phoenix websites, I use a standard path like “/home/username/mysite.com”, so I’m not sure how this gets setup for the socket and proxy pass. I tried commenting out the proxy pass / socket stuff as well, but letsencrypt still fails.

Lastly, this is the error that comes up in the Phoenix logs:

[info] GET /.well-known/acme-challenge/_JUybFBVPXS5vJVzhokjsigd21GeR1V85JigusrWERQ
[debug] ** (Phoenix.Router.NoRouteError) no route found for GET /.well-known/acme-challenge/_JUybFBVPXS5vJVzhokjsigd21GeR1V85JigusrWERQ (ExchatWeb.Router)
    (exchat 0.1.0) lib/phoenix/router.ex:402: ExchatWeb.Router.call/2
    (exchat 0.1.0) lib/exchat_web/endpoint.ex:1: ExchatWeb.Endpoint.plug_builder_call/2
    (exchat 0.1.0) lib/plug/debugger.ex:136: ExchatWeb.Endpoint."call (overridable 3)"/2
    (exchat 0.1.0) lib/exchat_web/endpoint.ex:1: ExchatWeb.Endpoint.call/2
    (phoenix 1.5.12) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4
    (cowboy 2.9.0) /home/username/exchat/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
    (cowboy 2.9.0) /home/username/exchat/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
    (cowboy 2.9.0) /home/username/exchat/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
    (stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

How would you get this setup to work?

Thanks

Just a stab in the dark here. Do you have .well-known as a static dir in endpoint.ex?

I do not. I’m not exactly sure what I’m supposed to do for this LetsEncrypt thing. Here is what I have:

/lib/exchat_web/endpoint.ex

defmodule ExchatWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :exchat

@session_options [
    store: :cookie,
    key: "_exchat_key",
    signing_salt: "0MwdBngh"
  ]

  socket "/socket", ExchatWeb.UserSocket,
    websocket: true,
    longpoll: false

  socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]

  # Serve at "/" the static files from "priv/static" directory.
  #
  # You should set gzip to true if you are running phx.digest
  # when deploying your static files in production.
  plug Plug.Static,
    at: "/",
    from: :exchat,
    gzip: false,
    only: ~w(css fonts images js favicon.ico robots.txt)

  # Code reloading can be explicitly enabled under the
  # :code_reloader configuration of your endpoint.
  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
    plug Phoenix.Ecto.CheckRepoStatus, otp_app: :exchat
  end

  plug Phoenix.LiveDashboard.RequestLogger,
    param_key: "request_logger",
    cookie_key: "request_logger"

  plug Plug.RequestId
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()

  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, @session_options

  plug ExchatWeb.Router
end

Also: /lib/exchat_web/router.ex

defmodule ExchatWeb.Router do
  use ExchatWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", ExchatWeb do
    pipe_through :browser

    get "/", PageController, :index
  end


  scope "/ex", ExchatWeb do
    pipe_through :api

    post "/users/login", UserController, :login
    post "/users/anon-login", UserController, :anon_login
  end


  # Enables LiveDashboard only for development
  #
  # If you want to use the LiveDashboard in production, you should put
  # it behind authentication and allow only admins to access it.
  # If your application does not have an admins-only section yet,
  # you can use Plug.BasicAuth to set up some basic authentication
  # as long as you are also using SSL (which you should anyway).
  if Mix.env() in [:dev, :test, :prod] do
    import Phoenix.LiveDashboard.Router

    scope "/" do
      pipe_through :browser
      live_dashboard "/dashboard", metrics: ExchatWeb.Telemetry
    end
  end
end

I’ll need to know what to put into these files, and then what the LetsEncrypt command should be for the webroot path variable. It is tricky because these paths in the Phoenix app aren’t like normal paths in the Ubuntu operating system, which is what I usually use for LetsEncrypt

sudo certbot --authenticator webroot --webroot-path [what path to put here?] --installer apache -n -d ex.mysite.com

In endpoint.ex, with respect to .well-known, I am using it to access apple-app-site-association file in there and this is how I successfully access it(note the “~w” line):

  # Serve at "/" the static files from "priv/static" directory.
  #
  # You should set gzip to true if you are running phx.digest
  # when deploying your static files in production.
  plug Plug.Static,
    at: "/",
    from: :faithful_word,
    gzip: false,
    only: ~w(css fonts images js favicon.ico robots.txt .well-known uploads)

Thanks for the hint. Are you using a sub-domain for your setup?

I added .well-known to the “~w” line and ran the LetsEncrypt call:

sudo certbot certonly --webroot --webroot-path / -n -d ex.mysite.com --deploy-hook "systemctl reload apache2" --dry-run

but it’s still not working. The LetsEncrypt error comes up as:

   Domain: ex.mysite.com
   Type:   unauthorized
   Detail: Invalid response from
   https://ex.mysite.com/.well-known/acme-challenge/AmMMHM5SRA-EBC9u7VFlsERT3FdZBFvyvUy49nTuw40
   [***.***.***.***]: "<!DOCTYPE html>\n<html>\n<head>\n    <meta
   charset=\"utf-8\">\n    <title>Phoenix.Router.NoRouteError at GET
   /.well-known/acme-challen"

Phoenix error:

[info] GET /.well-known/acme-challenge/AmMMHM5SRA-EBC9u7VFlsERT3FdZBFvyvUy49nTuw40
[debug] ** (Phoenix.Router.NoRouteError) no route found for GET /.well-known/acme-challenge/AmMMHM5SRA-EBC9u7VFlsERT3FdZBFvyvUy49nTuw40 (ExchatWeb.Router)
    (exchat 0.1.0) lib/phoenix/router.ex:402: ExchatWeb.Router.call/2
    (exchat 0.1.0) lib/exchat_web/endpoint.ex:1: ExchatWeb.Endpoint.plug_builder_call/2
    (exchat 0.1.0) lib/plug/debugger.ex:136: ExchatWeb.Endpoint."call (overridable 3)"/2
    (exchat 0.1.0) lib/exchat_web/endpoint.ex:1: ExchatWeb.Endpoint.call/2
    (phoenix 1.5.12) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4
    (cowboy 2.9.0) /home/username/exchat/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
    (cowboy 2.9.0) /home/username/exchat/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
    (cowboy 2.9.0) /home/username/exchat/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
    (stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

How about you try making a trivial mytestfile text file in .well-known and try accessing it over http?

I’m not even sure how to create a text file in .well-known inside Phoenix’s system. It’s not like the normal linux system that I am used to where you can just mkdir .well-known and vim mytestfile inside it. ‘.well-known’ inside phoenix isn’t even part of the linux filesystem for the certbot call. There are no tutorials or examples online (that I’ve found) for setting up LetsEncrypt on a subdomain / proxy pass system in apache.

Ok, I did a little searching and came across your earlier post: Serving .well-known/apple-app-site-association Works in Development env but returns Phoenix.HTML.Safe error in Release - #3 by maz

So I was able to add a directory:

/assets/static/.well-known/test.txt
/assets/static/.well-known/acme-challenge/test.txt

I’m still getting this error when running certbot:

[info] GET /.well-known/acme-challenge/_B_tSrUELGkILDnSgQJJxj6FHSVsW5zmiHx3T_dTGV4
[debug] ** (Phoenix.Router.NoRouteError) no route found for GET /.well-known/acme-challenge/_B_tSrUELGkILDnSgQJJxj6FHSVsW5zmiHx3T_dTGV4 (ExchatWeb.Router)

can you access .well-known/acme-challenge/test.txt ?

Edited

I got the HTTP version working so I could test the files. At the moment, I can access a file such as:

http://ex.mysite.com/.well-known/acme-challenge/test.txt

saved in linux filesystem at:

/home/username/app/assets/static/.well-known/acme-challenge/test.txt

However, that is a static file. certbot is trying to create and access a randomly generated file that it creates:

/.well-known/acme-challenge/SA9wcsbj4wLBl1OfSsb75bQQl9hTuHwzRlk7rEgp3bE

and I end up with this error:

[debug] ** (Phoenix.Router.NoRouteError) no route found for GET /.well-known/acme-challenge/osotlOoymnQdsQfpEnsTNrHTUwmHycKJT6v0Q6lXRms (ExchatWeb.Router)

How do we get certbot / LetsEncrypt to create the file and do the tests so I can get an SSL certificate?

Can you access .well-known/test.txt On prod? If you don’t get a Route error here then it’s not a router issue.

Another idea might be to add acme-challenge to the plug(only: ~w) strings.

Another thing that could be an issue is the permissions of .well-known and acme-challenge

Thanks for the help. I was able to fix this using the router.

No problem. Can you share your fix so when someone discovers this thread they can get up-and-running quickly?

Sure, no problem. Basically, I came across a good solution here: web deployment - Elixir Phoenix production server has issue with Letsencrypt renewal - Stack Overflow

In the router.ex file, I set up a snipet for the .well-known directory:

  scope "/.well-known", MyAppWeb do
   pipe_through :browser

   get "/acme-challenge/:challenge", AcmeChallengeController, :show
  end

and then a new controller letsencrypt.ex:

  defmodule MyAppWeb.AcmeChallengeController do
   use MyAppWeb, :controller

   def show(conn, %{"challenge" => "the_random_file_name"}) do
      send_resp(conn, 200, "TheHashInTheFile")
   end

   def show(conn, _) do
      send_resp(conn, 200, "Not valid")
   end
end

Lastly, you’ll need to do the certbot call using the /assets/static (or /priv/static) directory as the web root:

sudo certbot --authenticator webroot --webroot-path /home/username/myapp/assets/static --installer apache -n -d ex.mysite.com

Once that is done, everything was made fun for the long haul.

1 Like