OpenWrt 21.02: LuCi on Apache + SSL/TLS Wildcard certificates with acme package

I wanted to switch from uhttpd to Apache, because I could use it for hosting LuCi, and also to reverse-proxy to my home server and add SSL/TLS security.
At first, I saw this old topic, and found out that indeed, there is no proper guide how to make the webserver and LuCi work.

To generate certificates I am using acme, which can be downloaded as a package in OpenWRT. Unfortunately, it’s not so well documented yet. Hopefully, this will help those who couldn't configure it with uci yet.

I’m not an expert and struggled a lot to make this work. Therefore I want to share a small guide of what I have done. If somebody with more experience sees this, any suggestion is welcome toti improve this

1. Install the packages:

apache
apache-mod-proxy
apache-mod-ssl

acme
acme-dnsapi

Install them from the interface, or terminal, how you prefer.

2. Apache configuration
Apache configuration is created in /etc/apache. The extra directory contains some configuration examples. I made a backup of the original apache2.conf there.

Here is my apache2.conf:

ServerRoot "/usr"

Listen _RouterLocalIP_:80
Listen 443

LoadModule mpm_prefork_module lib/apache2/mod_mpm_prefork.so
LoadModule authn_file_module lib/apache2/mod_authn_file.so
LoadModule authn_core_module lib/apache2/mod_authn_core.so
LoadModule authz_host_module lib/apache2/mod_authz_host.so
LoadModule authz_groupfile_module lib/apache2/mod_authz_groupfile.so
LoadModule authz_user_module lib/apache2/mod_authz_user.so
LoadModule authz_core_module lib/apache2/mod_authz_core.so
LoadModule access_compat_module lib/apache2/mod_access_compat.so
LoadModule auth_basic_module lib/apache2/mod_auth_basic.so
LoadModule watchdog_module lib/apache2/mod_watchdog.so
LoadModule reqtimeout_module lib/apache2/mod_reqtimeout.so
LoadModule filter_module lib/apache2/mod_filter.so
LoadModule mime_module lib/apache2/mod_mime.so
LoadModule log_config_module lib/apache2/mod_log_config.so
LoadModule env_module lib/apache2/mod_env.so
LoadModule headers_module lib/apache2/mod_headers.so
LoadModule setenvif_module lib/apache2/mod_setenvif.so
LoadModule version_module lib/apache2/mod_version.so

LoadModule proxy_module lib/apache2/mod_proxy.so
LoadModule proxy_http_module lib/apache2/mod_proxy_http.so
LoadModule proxy_wstunnel_module lib/apache2/mod_proxy_wstunnel.so
LoadModule ssl_module lib/apache2/mod_ssl.so

LoadModule unixd_module lib/apache2/mod_unixd.so
LoadModule status_module lib/apache2/mod_status.so
LoadModule autoindex_module lib/apache2/mod_autoindex.so
LoadModule cgi_module lib/apache2/mod_cgi.so
LoadModule dir_module lib/apache2/mod_dir.so
LoadModule alias_module lib/apache2/mod_alias.so

LoadModule rewrite_module lib/apache2/mod_rewrite.so

<IfModule unixd_module>
#
# If you wish httpd to run as a different user or group, you must run
# httpd as root initially and it will switch.  
#
# User/Group: The name (or #number) of the user/group to run httpd as.
# It is usually good practice to create a dedicated user and group for
# running httpd, as with most system services.
#

#LuCI needs root privileges to access configuration
# User apache
# Group apache

</IfModule>

<IfModule dir_module>
    DirectoryIndex index.html
</IfModule>

<Files ".ht*">
    Require all denied
</Files>

#
# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
# container, error messages relating to that virtual host will be
# logged here.  If you *do* define an error logfile for a <VirtualHost>
# container, that host's errors will be logged there and not here.
#
ErrorLog "/tmp/apache.log"

#
# LogLevel: Control the number of messages logged to the error_log.
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
#
LogLevel error

<IfModule log_config_module>
    #
    # The following directives define some format nicknames for use with
    # a CustomLog directive (see below).
    #
    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
    LogFormat "%h %l %u %t \"%r\" %>s %b" common

    <IfModule logio_module>
      # You need to enable mod_logio.c to use %I and %O
      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
    </IfModule>

    #
    # The location and format of the access logfile (Common Logfile Format).
    # If you do not define any access logfiles within a <VirtualHost>
    # container, they will be logged here.  Contrariwise, if you *do*
    # define per-<VirtualHost> access logfiles, transactions will be
    # logged therein and *not* in this file.
    #
    CustomLog "/var/log/apache2/access_log" common

    #
    # If you prefer a logfile with access, agent, and referer information
    # (Combined Logfile Format) you can use the following directive.
    #
    #CustomLog "/var/log/apache2/access_log" combined
</IfModule>

<IfModule alias_module>
    ScriptAlias /cgi-bin/ "/www/cgi-bin/"
</IfModule>

<IfModule headers_module>
    #
    # Avoid passing HTTP_PROXY environment to CGI's on this or any proxied
    # backend servers which have lingering "httpoxy" defects.
    # 'Proxy' request header is undefined by the IETF, not listed by IANA
    #
    RequestHeader unset Proxy early
</IfModule>

<IfModule mime_module>
    #
    # TypesConfig points to the file containing the list of mappings from
    # filename extension to MIME-type.
    #
    TypesConfig /etc/apache2/mime.types

    #
    # AddType allows you to add to or override the MIME configuration
    # file specified in TypesConfig for specific file types.
    #
    #AddType application/x-gzip .tgz
    #
    # AddEncoding allows you to have certain browsers uncompress
    # information on the fly. Note: Not all browsers support this.
    #
    #AddEncoding x-compress .Z
    #AddEncoding x-gzip .gz .tgz
    #
    # If the AddEncoding directives above are commented-out, then you
    # probably should define those extensions to indicate media types:
    #
    AddType application/x-compress .Z
    AddType application/x-gzip .gz .tgz

    #
    # AddHandler allows you to map certain file extensions to "handlers":
    # actions unrelated to filetype. These can be either built into the server
    # or added with the Action directive (see below)
    #
    # To use CGI scripts outside of ScriptAliased directories:
    # (You will also need to add "ExecCGI" to the "Options" directive.)
    #
    #AddHandler cgi-script .cgi

    # For type maps (negotiated resources):
    #AddHandler type-map var

    #
    # Filters allow you to process content before it is sent to the client.
    #
    # To parse .shtml files for server-side includes (SSI):
    # (You will also need to add "Includes" to the "Options" directive.)
    #
    #AddType text/html .shtml
    #AddOutputFilter INCLUDES .shtml
</IfModule>

<IfModule proxy_html_module>
Include /etc/apache2/extra/proxy-html.conf
</IfModule>

ServerAdmin _YourEmail_
ServerName _RouterLocalIP_

ServerTokens Prod
ServerSignature Off

<Directory />
    AllowOverride none
    Require all denied
</Directory>

DocumentRoot "/www"
<Directory "/www">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require ip _YourLocalNetwork_
</Directory>

<Directory "/www/cgi-bin">
    AllowOverride None
    Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
    Require ip _YourLocalNetwork_
</Directory>

<VirtualHost *:443>
    ServerName _subdomain.subdomain.domain.com_

    SSLEngine on
    SSLCertificateFile "/etc/acme/_subdomain.domain.com_/_subdomain.domain.com_.cer"
    SSLCertificateChainFile "/etc/acme/_subdomain.domain.com_/fullchain.cer"
    SSLCertificateKeyFile "/etc/acme/_subdomain.domain.com_/_subdomain.domain.com_.key"

    DocumentRoot "/www"
    <Directory "/www">
        AuthType Basic
        AuthName "Restricted Content"
        AuthUserFile /etc/apache2/.htpasswd
        Require user _userGenerated_
        Options Indexes FollowSymLinks
        AllowOverride None
    </Directory>

    <Directory "/www/cgi-bin">
        AllowOverride None
        Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
        Require all granted
    </Directory>
</VirtualHost>

<VirtualHost *:443>
    ServerName _subdomain.domain.com_

    SSLEngine on
    SSLCertificateFile "/etc/acme/_subdomain.domain.com_/_subdomain.domain.com_.cer"
    SSLCertificateChainFile "/etc/acme/_subdomain.domain.com_/fullchain.cer"
    SSLCertificateKeyFile "/etc/acme/_subdomain.domain.com_/_subdomain.domain.com_.key"

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-XSS-Protection "1; mode=block"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "same-origin"

    ProxyPreserveHost On
    ProxyRequests off
    ProxyPass /api/websocket ws://_hostLocalIP_:8123/api/websocket
    ProxyPassReverse /api/websocket ws://_hostLocalIP_:8123/api/websocket
    ProxyPass / http://_hostLocalIP_:8123/
    ProxyPassReverse / http://_hostLocalIP_:8123/

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

<VirtualHost *:443>
     ServerName _subdomain.subdomain.domain.com_

    SSLEngine on
    SSLCertificateFile "/etc/acme/_subdomain.domain.com_/_subdomain.domain.com_.cer"
    SSLCertificateChainFile "/etc/acme/_subdomain.domain.com_/fullchain.cer"
    SSLCertificateKeyFile "/etc/acme/_subdomain.domain.com_/_subdomain.domain.com_.key"

    ProxyPreserveHost On
    ProxyPass / http://_hostLocalIP_:_port_/_customLocation/
    `ProxyPassReverse` / http://_hostLocalIP_:_port_/_customLocation_/

    <Location "/">
        AuthType Basic
        AuthName "Restricted Content"
        AuthUserFile /etc/apache2/.htpasswd
        Require user _userGenerated_
    </Location>
</VirtualHost>

I wanted that LuCi to be accessible via http only from local network, this is why I bound the router's local IP to the 80 port (Listen _RouterLocalIP_:80). For more safety, I added Require ip _YourLocalNetwork_ to the directories, to make sure it's accessible only from the local network. Put there for ex. 192.168.1, the bytes that stands for your network address.

So this apache configuration, without the <VirtualHost *:443> directives will respond only to local http requests. The first VirtualHost is how I am accessing LuCi externally via https, the second one is a reverse-proxy to my Home Assistant server, a third one is a generic example, how a basic reverse-proxy works.
I added additional authentication to certain pages, you can also generate users with these commands:

        htpasswd -c /etc/apache2/.htpasswd _userGenerated_
        htpasswd /etc/apache2/.htpasswd _userGenerated2_

The option -c is needed only the first time, to create the specified file.
Otherwise, delete these lines:

        AuthType Basic
        AuthName "Restricted Content"
        AuthUserFile /etc/apache2/.htpasswd
        Require user _userGenerated_

Don't forget the / at the end of every address at the ProxyPass and ProxyPassReverse directives!

3. ACME configuration
When using ACME from OpenWRT you have to configure the parameters via uci, and then the acme service will call the acme.sh periodically based on your configuration. Sadly, the uci options are not documented. I installed luci-app-acme, made a working config from the interface, and then I looked at the /etc/config/acme configuration. Here's mine:


config acme
	option state_dir '/etc/acme'
	option account_email '_youEmail_'
	option debug '1'

config cert 'dynu_wildcard'
	option validation_method 'dns'
	option update_uhttpd '0'
	option days '60'
	option use_staging '0'
	option keylength '2048'
	option dns 'dns_dynu'
	option enabled '1'
  option use_staging '0'
	list domains '_subdomain.domain.com_'
	list domains '*._subdomain.domain.com_'

As you saw, I'm accessing my services by different subdomains, this is why I needed a wildcard certificate. Before this I was using duckdns, but after hours of investigations I realized that their API is not compatible with acme, and could not generate a wildcard certificate. I moved to dynu after that.

The apache configuration can read the certificates directly from /etc/acme/..., so basically that's it!

Don't forget to stop and disable (prevent to autostart at reboot) uhttpd.

Useful info:

/etc/init.d/uhttpd stop
/etc/init.d/uhttpd disable

/etc/init.d/apache2 start/stop/restart
/etc/init.d/acme start/stop/restart

apache log location (with this config): /tmp/apache.log

NOTE: In this guide, I read that

LuCI needs root privileges to access configuration, so lighttpd needs to run as root too, so edit the configuration file /etc/lighttpd/lighttpd.conf

so I commented out

# User root
# Group root`

lines in apache2.conf, in order to have root privileges. Because of that the apache log contains entries of these:

 [unixd:alert] [pid 2938] (2)No such file or directory: AH02155: getpwuid: couldn't determine user name from uid -1, you probably need to modify the User directive

Does somebody know, how should this be corrected??

Shouldn't it be the other way around ?

uncomment, for it to run as root ?

from httpd.conf on Fedora:

#
# If you wish httpd to run as a different user or group, you must run
# httpd as root initially and it will switch.
#
# User/Group: The name (or #number) of the user/group to run httpd as.
# It is usually good practice to create a dedicated user and group for
# running httpd, as with most system services.
#
User apache
Group apache

Originally it was as you quoted, but this way the server has not enough permissions. If you specify the root user & group, the service throws an error, and doesn't even start. This is why I didn't specify anything, by commenting them out.

I was making some tests, and it seems that I haven't reverted these lines after that. I will correct my post above, thanks.

It's recommended to create a dummy VHost at the first place in the list:

<VirtualHost *:443>
    ServerName dummy

    SSLEngine on
    SSLCertificateFile "/etc/acme/vampire.ooguy.com/vampire.ooguy.com.cer"
    SSLCertificateChainFile "/etc/acme/vampire.ooguy.com/fullchain.cer"
    SSLCertificateKeyFile "/etc/acme/vampire.ooguy.com/vampire.ooguy.com.key"

    RewriteEngine On
    RewriteRule .* - [G]
</VirtualHost>

This way, Error 410 (Gone) will be responded to every unhandled subdomain requests.