My website is a secure Express.js web application that only wants to have encrypted conversations over HTTPS with your web browser. It listens to traffic on the Internet on TCP port 443. The little lock icon that you see in your browser's address bar indicates that you are reading this article over secure, encrypted HTTPS. Everything is working right.

You might try to force your browser to have an unecrypted and insecure conversation with my website through HTTP. To prevent that I use a web server called lighttpd to send a special message called a 301 redirect to your browser that asks it to re-connect to my website on the secure, encrypted port (TCP port 443).

To facilitate these nice, secret, encrypted conversations I must present to your browser a certificate that your browser trusts. If your browser trusts the certificate then it will agree to have an encrypted conversation.

One way to get a trusted certificate is to use a service like Let's Encrypt using the ACME protocol to provide one for you.

The trouble is: the ACME protocol must communicate with subscribing websites through HTTP, on the insecure port 80. So, how can my web app tell your browser to only talk to it over secure HTTPS (port 443) and at the same time allow Let's Encrypt and the ACME protocol to talk insecurely?

This is what this article will discuss. Here's a sketch of how I do it:

How lighttpd can support a secure expressjs app listening on 443 and certbot which wants to talk to port 80
[Fig.1 - a sketch of how lighttpd can be configured.]

Now, let's explain what's going on.

Redirecting Port 80 To Your Secure Express.js App While Listening for ACME on Port 80

First, we should note this simple construct:

  1. lighttpd is only listening on port 80 (http)
  2. My Express.js web app is only listening on port 443 (https)

So, anything that the lighttpd web server hears is always HTTP.

Thus, the default thing that the lighttpd server needs to do is simply redirect HTTP requests hitting port 80 to port 443 where the Express.js web app will handle it from there.

But we have to build a tiny exception to the above:

The ACME protocol is the thing that sets up a simple test to prove that I am in control of my website. If I have proper control over my website I will be granted a certificate that my Express.js app can use to establish secure communication. There are several ways an ACME client can be used to do this but one of the most common ways works like this:

If You Really Own This Website Then:

  1. The ACME client will place an unknown file in .well-known/acme-challenge/ off of your webroot on your domain
  2. The ACME client will then ask an external server to ask for that file from your domain at: http://your_domain/.well-known/acme-challenge/
  3. If the external server finds the secret file then we can confirm that you have control over your domain and a certificate can be granted.

So, this whole ACME verification thing works exclusively over HTTP, port 80.

That's why our goal is to redirect requests to my web site for port 80 to port 443 while at the same time listening and serving requests from Let's Encrypt/Certbot/ACME exclusively on port 80.

The idea here (per my drawing, above) is to configure lighttpd to be a mirror with a little pinhole in the middle of it. The mirror redirects all HTTP requests to HTTPS but allows a tiny, specific request that ACME uses through to a static web-root.

The Mirror

The mirror is built using lighttpd's mod_redirect module.

mod_redirect can be configured like this to bounce traffic from HTTP to HTTPS:

  url.redirect = (
    ".*" => "https://%0$0"

So, take the URL (%0) and tack a https:// in front of it and redirect to that.

The Pinhole

Now we need to build an exception so that ACME's request to:… will not bounce the request to HTTPS but simply direct the request to lighttpd itself to be handled.

I use lighttpd's mod_alias module to do this. Here's how it can be done:

$HTTP["url"] =~ "^/\.well\-known" {
  alias.url = ( "/.well-known/" => "/webroot/.well-known/" )

The block above says: "if the beginning of the URL is /.well-known, then use mod_alias to make the document root for this special request be some folder (/webroot/.well-known/) to be served by lighttpd.

Now, let's put these two directives - the mirror and the pinhole - together…

The Running Config

I chose to build an if/else construct in the lighttpd configuration file so that we first test for the special request from the ACME protocol (http://my_website/.well-known/acme-challenge/…) then let everything else be reflected by the 'mirror'.

$HTTP["url"] =~ "^/\.well\-known" {
  alias.url = ( "/.well-known/" => "/webroot/.well-known/" )
} else $HTTP["host"] =~ ".*" {
  url.redirect = (
    ".*" => "https://%0$0"

First, we look for the special URL: /.well-known/acme-challenge. If the request matches, then direct the request to a folder where the ACME client will be placing the secret files.

Otherwise, direct all requests to all domains to the https-version of the domain and url.

At this point you can restart your webserver and you will have proper 301 redirection with our little pinhole for future certificate management through something like the Certbot ACME client.

Invoking Cerbot

It might be helpful to show how I invoke the Cerbot ACME client so that it works nicely with the above setup:

To get your first certificate(s)

sudo certbot certonly --webroot -w /var/webroot/ -d

Perhaps the main key to the above is that I tell the certbot ACME client to place its secret files on the webroot: /var/webroot/.

Certbot will create, if missing, the .well-known/acme-challenge folders at the webroot specified and place the secret files there. This means that the webroot that you specify must match the web root configured in your lighttpd config.

To renew your certs simply do:

sudo certbot renew

If you want to test certificate renewal:

sudo certbot renew --dry-run


All of this shows how you might support your running Express.js website running on HTTPS while, at the same time, supporting certificate renewal through an ACME client.

Of course, you can always stop your website, start up a static web server, run your ACME client, then restart your website once your serts have been renewed but I liked the idea of not interrupting my site to perform this task.