Apache HTTP Server on FreeBSD

I have some static (or with light CGI) sites I thought I'd like to host on my FreeBSD server. Apache HTTP Server seems to be one of the popular ways to do this on FreeBSD. I think I last used Apache back in the 1.x days, so I figured a lot had probably changed. Reading through the documentation, I was pleased to find that a lot of nice improvements had been made.

Firewall

I'm using the pf firewall, I added the http and https ports to a rule in /etc/pf.conf that allows incoming connections, checked the configuration, and reloaded it.

pass in proto tcp to any port { ssh http https }
root@lucy:~ # pfctl -f /etc/pf.conf -n
root@lucy:~ # service pf reload
Reloading pf rules.

Installation

I started by installing the apache-24 package with pkg install apache24. I added an apache24_enable="YES" line to /etc/rc.conf, but didn't start Apache yet. I wanted to set up a minimal httpd.conf file first.

Minimal Configuration

I decided to write a configuration file for Apache from scratch so I'd know what all was in it and why. I figured this would be a good exercise to get familiar with Apache again after so long away.

I moved /usr/local/etc/apache24/httpd.conf to a backup file and started fresh. First I configured the core module. FreeBSD can do some magic in the kernel before passing a socket off to Apache. I didn't have the kernel modules for this enabled, so I disabled the supporting options in Apache so it wouldn't be confused. I planned to use name-based virtual hosts, but I wanted to have a fallback site to show some (hopefully) helpful information to anyone who arrived at my server via a host name that wasn't configured as a virtual host. I made a new directory /usr/local/www/default to hold this default site's files. The server name and admin contact information are used by Apache to build its default error pages. I might add nicer pages for some or all of my sites later, but in the meantime I didn't want to leave folks hanging without this information.

# Core module
AcceptFilter http none
AcceptFilter https none
DocumentRoot www/default
ServerAdmin support@parksdigital.com
ServerName vultr1.parksdigital.com

The authz_core module seems to be needed to handle an implicit require all granted directive on DocumentRoot (or Location /?). The dir module would let me serve index.html for URLs that end in a directory. The MIME module sets the content-type header automatically based on the extension of the served file. Apache now has a choice of Multi-Processing Modules for handling the basic tasks of binding network ports, accepting requests, and dispatching children to handle the requests. The Event MPM seems to be popular and a good fit for FreeBSD. Finally, I'd need the UNIXd module for changing to the www user after starting up (indeed, Apache will shut back down if it finds it's still running as root after startup).

# SO module
LoadModule authz_core_module libexec/apache24/mod_authz_core.so
LoadModule dir_module libexec/apache24/mod_dir.so
LoadModule mime_module libexec/apache24/mod_mime.so
LoadModule mpm_event_module libexec/apache24/mod_mpm_event.so
LoadModule unixd_module libexec/apache24/mod_unixd.so

I configured the event module to listen on port 80 of my server's public address.

# Event module
Listen 69.63.227.51:80

And I configured the UNIXd module to change to the www user and group during startup.

# UNIXd module
Group www
User www

I started the server up with service apache24 start.

Virtual Hosts

The first VirtualHost section in the configuration will be used as the default if none of the others have a ServerName or ServerAlias directive which match the Host header of the request. I wanted to use the base server configuration as the fallback in this case, so I added an empty VirtualHost section for port 80 which would catch these requests and inherit the base server configuration.

<VirtualHost *:80>
</VirtualHost>

I knew I'd have several virtual hosts, so I used the Macro module to simplify the configuration. Each virtual host would be served from a directory with the same name as the site. It's hard to guess how folks will type in the address of your site, so I wanted to catch both the bare domain name and the www host. My preference is to redirect www to the bare domain, but you could go the other way too. I used the Rewrite module for this task. There are other ways to do it, but I think I came up with a neat solution.

# SO module
LoadModule macro_module libexec/apache24/mod_macro.so
LoadModule rewrite_module libexec/apache24/mod_rewrite.so

# Rewrite module
RewriteOptions InheritDown
RewriteCond %{HTTP_HOST} ^www\.(.+) [NC]
RewriteRule (.*) %{REQUEST_SCHEME}://%1$1 [R=301]

# Name virtual hosts

<Macro VHost $name>

<VirtualHost *:80>
DocumentRoot www/$name
ServerName $name
ServerAlias www.$name
RewriteEngine on
</VirtualHost>

</Macro>

Use VHost parksdigital.com
Use VHost gypsyroadstables.com
Use VHost heartfx.net
Use VHost neurostrategy.org
Use VHost wirs.parksdigital.com

I made directories for each static site and set the ownership, then uploaded the site files.

for site in parksdigital.com gypsyroadstables.com heartfx.net \
		neurostrategy.org wirs.parksdigital.com ; do
	mkdir /usr/local/www/$site
	chown user:www /usr/local/www/$site
done

After checking the configuration file with httpd -t, I reloaded the configuration with service apache24 reload.

Common Gateway Interface

A couple of my sites include some modest CGI scripts. To run them, I used Apache's CGId module. I also loaded the Env module for passing configuration to the scripts through environment variables. Since this configuration is sometimes secret, I changed the mode on httpd.conf so only the owner (root) can read and write it. The ScriptLog directive allows useful debugging information to be logged when something goes wrong with a script. Since this information might also contain sensitive information, I set its mode so that it can be read and written only by its owner (root) and read only by members of its group (wheel). Once everything was set, I tested the configuration and restarted Apache (usually I would just reload it, but in this case I think the restart is required to get the CGI daemon running).

# SO module
LoadModule cgid_module libexec/apache24/mod_cgid.so
LoadModule env_module libexec/apache24/mod_env.so

# CGId module
ScriptLog /var/log/httpd-script.log

# CGI script directories

<Directory /usr/local/www/*/cgi>
Options +ExecCGI
SetHandler cgi-script
</Directory>

<Directory /usr/local/www/parksdigital.com/cgi>
SetEnv TWILIO_ACCOUNT_SID ***
SetEnv TWILIO_AUTH_TOKEN ***
</Directory>

<Directory /usr/local/www/gypsyroadstables.com/cgi>
SetEnv TWILIO_ACCOUNT_SID ***
SetEnv TWILIO_AUTH_TOKEN ***
</Directory>
chmod 600 /usr/local/etc/apache24/httpd.conf
touch /var/log/httpd-script.log
chmod 640 /var/log/httpd-script.log
httpd -t
service apache24 restart

TLS Certificates

I knew I'd want TLS available for my sites, so I also installed certbot with pkg install py38-certbot. I've been pretty happy in the past with how certbot works, but I don't love that it brings in Python and a lot of other dependencies. After looking at some of the other options, I decided I liked it the best out of the bunch anyway.

I figured I'd use certbot's webroot authentication method. I thought it would be nice to have a single directory for certbot to use for all sites, so I wouldn't have to have a site set up in httpd.conf before requesting a certificate for the first time. Otherwise I'd have to set up the site in two stages: first without TLS to get the certificate then with TLS to use the certificate. I made a directory for certbot and used the Alias module in Apache to get what I was after.

mkdir -p /usr/local/www/certbot/.well-known/acme-challenge
# SO module
LoadModule alias_module libexec/apache24/mod_alias.so

# Alias module
Alias /.well-known/acme-challenge www/certbot/.well-known/acme-challenge

I registered an account and requested certificates for each of my sites:

certbot register -m aparks@aftermath.net --agree-tos --no-eff-email
certbot certonly --webroot -w /usr/local/www/certbot/ \
	--cert-name lucy.parksdigital.com \
	-d lucy.parksdigital.com
certbot certonly --webroot -w /usr/local/www/certbot/ \
	--cert-name parksdigital.com \
	-d parksdigital.com -d www.parksdigital.com
certbot certonly --webroot -w /usr/local/www/certbot/ \
	--cert-name gypsyroadstables.com \
	-d gypsyroadstables.com -d www.gypsyroadstables.com
certbot certonly --webroot -w /usr/local/www/certbot/ \
	--cert-name heartfx.net \
	-d heartfx.net -d www.heartfx.net
certbot certonly --webroot -w /usr/local/www/certbot/ \
	--cert-name neurostrategy.org \
	-d neurostrategy.org -d www.neurostrategy.org
certbot certonly --webroot -w /usr/local/www/certbot/ \
	--cert-name wirs.parksdigital.com \
	-d wirs.parksdigital.com

To get automatic certificate renewals, I added a weekly_certbot_enable="YES" line to /etc/periodic.conf.

It took some puzzling over the Apache and OpenSSL documentation along with some experimentation with SSL Labs test tool, but I came up with a configuration for Apache's SSL module which seems to get a good score while supporting a broad range of devices. I'm not completely certain that the SSLRandomSeed directives are necessary, but the documentation made it sound like Apache would use a questionable internal RNG if it's not given. I'd feel better if I had some guidance or basis for the number of bytes to read, too. The value I used seems to be common and, but I wasn't able to find the reasoning behind it. In my (poorly informed) judgment it's probably enough.

# SO module
LoadModule ssl_module libexec/apache24/mod_ssl.so

# Event module
Listen 104.207.142.72:443

# SSL module
SSLCipherSuite "ECDHE-ECDSA-AES128-GCM-SHA256:\
ECDHE-ECDSA-AES256-GCM-SHA384:\
ECDHE-ECDSA-AES128-SHA:\
ECDHE-ECDSA-AES256-SHA:\
ECDHE-ECDSA-AES128-SHA256:\
ECDHE-ECDSA-AES256-SHA384:\
ECDHE-RSA-AES128-GCM-SHA256:\
ECDHE-RSA-AES256-GCM-SHA384:\
ECDHE-RSA-AES128-SHA:\
ECDHE-RSA-AES256-SHA:\
ECDHE-RSA-AES128-SHA256:\
ECDHE-RSA-AES256-SHA384:\
DHE-RSA-AES128-GCM-SHA256:\
DHE-RSA-AES256-GCM-SHA384:\
DHE-RSA-AES128-SHA:\
DHE-RSA-AES256-SHA:\
DHE-RSA-AES128-SHA256:\
DHE-RSA-AES256-SHA256"
SSLHonorCipherOrder on
SSLProtocol all -TLSv1.1 -TLSv1
SSLRandomSeed startup file:/dev/urandom 512
SSLRandomSeed connect file:/dev/urandom 512

I added a default virtual host for TLS connections. This is a bit less useful than the default for regular connections since folks coming to a host whose name isn't set on one of the name virtual hosts will get a certificate warning, but I guess it's better to get the default site after the certificate warning rather than something unrelated.

# Core module
Define cert_dir etc/letsencrypt/live

# Default virtual host

<VirtualHost *:443>
SSLCertificateFile ${cert_dir}/vultr1.parksdigital.com/fullchain.pem
SSLCertificateKeyFile ${cert_dir}/vultr1.parksdigital.com/privkey.pem
SSLEngine on
</VirtualHost>

Finally, I added a second VirtualHost directive to the VHost macro so that each site would have the option of TLS. At present, none of these sites require TLS but I like to provide the option for folks who would like it and so folks who are running HTTPS-everywhere don't get stuck (HTTPS-everywhere tries to use HTTPS if port 443 is listening; it doesn't have a way to check that the server is configured with a certificate for the particular name-virtual-host it's trying to connect to).

# Name virtual hosts

<Macro VHost $name>

...

<VirtualHost *:443>
DocumentRoot www/$name
ServerName $name
ServerAlias www.$name
RewriteEngine on
SSLCertificateFile ${cert_dir}/$name/fullchain.pem
SSLCertificateKeyFile ${cert_dir}/$name/privkey.pem
SSLEngine on
</VirtualHost>

Log Rotation

FreeBSD has newsyslog as part of the base system for log rotation. It looks in /etc/newsyslog.conf.d as well as /usr/local/etc/newsyslog.conf.d for application-specific configuration files. I had to create the latter directory myself. In there I made a file called apache24.conf and added configuration to rotate both the error and script logs daily and keep seven days worth of them. I also set the configuration to send SIGUSR1 to Apache after rotating the log files. This will cause it to do a graceful restart, re-opening the new (empty) log files.

/var/log/httpd-error.log  640 7 * @T00 - /var/run/httpd.pid SIGUSR1
/var/log/httpd-script.log 640 7 * @T00 - /var/run/httpd.pid SIGUSR1

In Conclusion

I hope that this has been helpful to you. If you found it interesting, you may also enjoy my other work. If you have any questions, corrections, or comments please drop me a line.

Aaron D. Parks
aparks@aftermath.net