Setup Apache 2 proxy for Home Assistant with Let's Encrypt in webroot mode

17/03/2019 - Home Assistant, Apache, LetsEncrypt, webroot, Ubuntu 18.04

Steps to use Apache 2 as a proxy for Home Assistant with a certificate provided by Let's Encrypt. Let's Encrypt is run in webroot mode, which relies on creating a temporary file in a directory which is served by Apache. This means that the process to get and renew certificates doesn't need to modify the Apache configuration or stop/start the server. More details are given here.

Apache setup

Install Apache, enable the required Apache modules and restart Apache:

sudo apt install apache2
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
sudo a2enmod rewrite
sudo a2enmod ssl
sudo systemctl restart apache2

I prefer to redirect all URLs to https in the default VirtualHost, and to only have https VirtualHosts otherwise. The config in /etc/apache2/sites-available/000-default.conf looks like:

<VirtualHost *:80>
  ServerAdmin [email protected]
  DocumentRoot /var/www/html

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined

  ServerSignature Off
  RewriteEngine On
  RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent]
</VirtualHost>

I add a new virtual host as /etc/apache2/sites-available/001-ha.conf with the proxy configuration as given here, but also set to not redirect the .well-known directory used by Let's Encrypt in webroot mode. This assumes that the site is available at ha.my.domain:

<VirtualHost *:80>
  ServerName ha.my.domain

  ServerAdmin [email protected]
  DocumentRoot /var/www/html

  ErrorLog ${APACHE_LOG_DIR}/ha-error.log
  CustomLog ${APACHE_LOG_DIR}/ha-access.log combined

  ProxyPreserveHost On
  ProxyRequests off
  ProxyPass /.well-known/ !
  ProxyPass / http://localhost:8123/
  ProxyPassReverse / http://localhost:8123/
  ProxyPass /api/websocket ws://localhost:8123/api/websocket
  ProxyPassReverse /api/websocket ws://localhost:8123/api/websocket

  RewriteEngine on
  RewriteCond %{REQUEST_URI} !^/.well-known/ [NC]
  RewriteCond %{HTTP:Upgrade} =websocket [NC]
  RewriteRule /(.*)  ws://localhost:8123/$1 [P,L]
  RewriteCond %{REQUEST_URI} !^/.well-known/ [NC]
  RewriteCond %{HTTP:Upgrade} !=websocket [NC]
  RewriteRule /(.*)  http://localhost:8123/$1 [P,L]
</VirtualHost>

And make it active (initially it is served unencrypted):

sudo ln -s /etc/apache2/sites-available/001-ha.conf /etc/apache2/sites-enabled
sudo systemctl restart apache2

Let's Encrypt

Install Certbot with the instructions here.

Run with (setting the webroot path to the same directory as in VirtualHost configuration):

sudo certbot certonly --webroot --webroot-path /var/www/html -d ha.my.domain

The output should be similar to this.

Because Certbot was run in certonly mode, you still have to finalise the Apache configuration yourself. The following is based on what the Certbot Apache plugin would do. Create a version of the VirtualHost that points to the new certificate, e.g. /etc/apache2/sites-available/001-ha-le-ssl.conf:

<IfModule mod_ssl.c>
<VirtualHost *:443>
  ServerName ha.my.domain

  ServerAdmin [email protected]
  DocumentRoot /var/www/html

  ErrorLog ${APACHE_LOG_DIR}/ha-error.log
  CustomLog ${APACHE_LOG_DIR}/ha-access.log combined

  ProxyPreserveHost On
  ProxyRequests off
  ProxyPass /.well-known/ !
  ProxyPass / http://localhost:8123/
  ProxyPassReverse / http://localhost:8123/
  ProxyPass /api/websocket ws://localhost:8123/api/websocket
  ProxyPassReverse /api/websocket ws://localhost:8123/api/websocket

  RewriteEngine on
  RewriteCond %{REQUEST_URI} !^/.well-known/ [NC]
  RewriteCond %{HTTP:Upgrade} =websocket [NC]
  RewriteRule /(.*)  ws://localhost:8123/$1 [P,L]
  RewriteCond %{REQUEST_URI} !^/.well-known/ [NC]
  RewriteCond %{HTTP:Upgrade} !=websocket [NC]
  RewriteRule /(.*)  http://localhost:8123/$1 [P,L]

  SSLCertificateFile /etc/letsencrypt/live/ha.my.domain/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/ha.my.domain/privkey.pem
  Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

Copy the options-ssl-apache.conf configuration file into place, disable the old VirtualHost, enable the new one and finally restart Apache:

sudo cp /usr/lib/python3/dist-packages/certbot_apache/options-ssl-apache.conf /etc/letsencrypt
sudo rm /etc/apache2/sites-enabled/001-ha.conf
sudo ln -s /etc/apache2/sites-available/001-ha-le-ssl.conf /etc/apache2/sites-enabled/001-ha-le-ssl.conf
sudo systemctl restart apache2

Test that certificate renewal works:

sudo certbot --dry-run renew

The output should be similar to this.

The Certbot package normally installs a systemd timer task to do the renewal:

$ systemctl status certbot.timer
‚óŹ certbot.timer - Run certbot twice daily
   Loaded: loaded (/lib/systemd/system/certbot.timer; enabled; vendor preset: enabled)
   Active: active (waiting) since Sun 2019-03-17 18:36:00 UTC; 3 days ago
  Trigger: Thu 2019-03-21 01:02:38 UTC; 3h 31min left

Mar 17 18:36:00 hostname systemd[1]: Started Run certbot twice daily.