Like many of you, I was dismayed to see that Apple has decided to effectively abandon the macOS Server app that I used to set up my simple static web sites. Once one updates to macOS Mojave, most of the features of the Server app are just gone, and one is left to fend for one's self in a maze of twistly little forum posts, all different, trying to figure out how to get things working again.
My use case is relatively simple: I'm not trying to run a mail server, or a wiki, or any sort of fancy active web content like WordPress and the like. I just had a giant pile of static content -- photos, mainly -- that I wanted to keep hosting with a minimum of fuss. (Most of it is of no interest to the public, but you might enjoy some high quality images of the Charters of Freedom.) I thought: surely, somebody somewhere has written a simple step-by-step guide for how to deal with this. And probably someone has, and I just wasn't able to find it. In the meantime, in the hope that the next poor schmuck who finds themselves in my shoes can benefit, here's a discussion of what I've done to get my static site working well again after updating from High Sierra (macOS 10.13) to Mojave (macOS 10.14).
The warning given by our MacStadium friends here if anything understates how much of a nuisance this update will be. I recommend doing a test run on an entirely separate and expendable Mac before you try updating any machine you care about. If you have a less expensive Mac like mine without solid state storage, be prepared to be off-line for at least four hours. Just installing Mojave and the subsequent updates will take that long.
That said, here is a step-by-step guide showing what worked for me. I hope you find it helpful. If it breaks your Mac, you get to keep both pieces: no warranty is express or implied.
# When letsencrypt tries to create your certificate, it will use plain # http to fetch some magic URLs that certbot will create for you behind # the scenes in the ".well-known/acme-challenge" directory. This proves # to letsencrypt that you control the domains you're getting certificates # for. The first "RewriteRule" below uses a "-" to do *nothing* to those # special letsencrypt requests, passing them unaltered through to the # (normally not present, but created temporarily by certbot) directory # /Volumes/4M1/adam/sites/nara/.well-known. # # All other plain http requests to nara.cyclecounters.org are rewritten # into equivalent requests to https://nara.cyclecounters.org by the # second RewriteRule. As a result, the DocumentRoot and Directory # for this host aren't used for anything else -- we only need them # when getting or renewing certificates. <VirtualHost *:80> ServerName http://nara.cyclecounters.org:80 RewriteEngine On RewriteCond %{SERVER_PORT} 80 RewriteRule ^/.well-known/(.*) - [L] RewriteRule ^(.*)$ https://nara.cyclecounters.org$1 [R,L] DocumentRoot "/Volumes/4M1/adam/sites/nara" <Directory "/Volumes/4M1/adam/sites/nara/.well-known"> Require all granted Options All -Indexes -ExecCGI -Includes +MultiViews AllowOverride none <IfModule mod_dav.c> DAV Off </IfModule> </Directory> </VirtualHost> # This second virtual host redirects anyone still using # http://www.nara.cyclecounters.org to the equivalent # http://nara.cyclecounters.org URL. We don't need any trickery # for certificates, since certbot will use different temporary # filenames in /Volumes/4M1/adam/sites/nara/.well-known/acrme-challenge # when establishing that we control both www.nara.cyclecounters.org # and nara.cyclecounters.org. (We could, in principle, share the # same ".well-known" directory among all sites on the machine if # we wished.) <VirtualHost *:80> ServerName http://www.nara.cyclecounters.org:80 RewriteEngine On RewriteCond %{SERVER_PORT} 80 RewriteRule ^(.*)$ http://nara.cyclecounters.org$1 [R,L] </VirtualHost> # We can't just start creating virtual hosts that use SSL until # we have valid certificates, but we need to serve something to # get the certificates in the first place, creating a chicken-and-egg # problem. We work around this by first disabling ssl in httpd.conf, # which turns off these two virtual hosts since the IfModule will no # longer be true. Then, once certbot has installed the certificates # (recall that letsencrypt will validate using plain http, not https), # we'll re-enable SSL in httpd.conf and restart apache. More on that # below. <IfModule ssl_module> # This third virtual host redirects anyone using # https://www.nara.cyclecounters.org to the equivalent # https://nara.cyclecounters.org URL. No trickery here, # but note that apache requires a valid set of SSL directives # here -- the redirect will be properly encrypted, so folks # using https://www.nara.cyclecounters.org should still be # safe from eavesdropping. When we create the cyclecounters.org # certificate, we request that certbot make the one certificate # valid for both nara.cyclecounters.org and www.nara.cyclecounters.org <VirtualHost *:443> ServerName https://www.nara.cyclecounters.org:443 RewriteEngine On RewriteCond %{SERVER_PORT} 443 RewriteRule ^(.*)$ https://nara.cyclecounters.org$1 [R,L] SSLEngine on SSLCertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/cert.pem" SSLCACertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/fullchain.pem" SSLCertificateKeyFile "/private/etc/letsencrypt/live/cyclecounters.org/privkey.pem" </VirtualHost> # Finally, the fourth virtual host has the actual content of interest. <VirtualHost *:443> ServerName https://nara.cyclecounters.org:443 DocumentRoot "/Volumes/4M1/adam/sites/nara" SSLEngine on SSLCertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/cert.pem" SSLCACertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/fullchain.pem" SSLCertificateKeyFile "/private/etc/letsencrypt/live/cyclecounters.org/privkey.pem" DirectoryIndex index.html <Directory "/Volumes/4M1/adam/sites/nara"> Require all granted Options All -Indexes -ExecCGI -Includes +MultiViews AllowOverride none <IfModule mod_dav.c> DAV Off </IfModule> </Directory> </VirtualHost> </IfModule>The same disclaimers apply: mean clever people will no doubt use this information against me, if you're a nice clever person and you see something stupid, please let me know, thanks!
<VirtualHost *:80> ServerName http://hilohigh.cyclecounters.org:80 RewriteEngine On RewriteCond %{SERVER_PORT} 80 RewriteRule ^/.well-known/(.*) - [L] RewriteRule ^(.*)$ https://hilohigh.cyclecounters.org$1 [R,L] DocumentRoot "/Volumes/4M1/adam/sites/hilohigh" <Directory "/Volumes/4M1/adam/sites/hilohigh/.well-known"> Require all granted Options All -Indexes -ExecCGI -Includes +MultiViews AllowOverride none <IfModule mod_dav.c> DAV Off </IfModule> </Directory> </VirtualHost> <VirtualHost *:80> ServerName http://www.hilohigh.cyclecounters.org:80 RewriteEngine On RewriteCond %{SERVER_PORT} 80 RewriteRule ^(.*)$ http://hilohigh.cyclecounters.org$1 [R,L] </VirtualHost> <IfModule ssl_module> <VirtualHost *:443> ServerName https://www.hilohigh.cyclecounters.org:443 RewriteEngine On RewriteCond %{SERVER_PORT} 443 RewriteRule ^(.*)$ https://hilohigh.cyclecounters.org$1 [R,L] SSLEngine on SSLCertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/cert.pem" SSLCACertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/fullchain.pem" SSLCertificateKeyFile "/private/etc/letsencrypt/live/cyclecounters.org/privkey.pem" </VirtualHost> # The fourth virtual host is the only one that needs to change; it uses # a different "require" line to turn on passwords, which are stored # outside the document root so they can't be downloaded and cracked offline. <VirtualHost *:443> ServerName https://hilohigh.cyclecounters.org:443 DocumentRoot "/Volumes/4M1/adam/sites/hilohigh" SSLEngine on SSLCertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/cert.pem" SSLCACertificateFile "/private/etc/letsencrypt/live/cyclecounters.org/fullchain.pem" SSLCertificateKeyFile "/private/etc/letsencrypt/live/cyclecounters.org/privkey.pem" DirectoryIndex index.html <Directory "/Volumes/4M1/adam/sites/hilohigh"> Options All -Indexes -ExecCGI -Includes +MultiViews AllowOverride none AuthUserFile /Volumes/4M1/adam/sites/hilohigh.htpasswd AuthName photos AuthType Basic require valid-user <IfModule mod_dav.c> DAV Off </IfModule> </Directory> </VirtualHost> </IfModule>
$ # Stop the currently running web server, if any. $ sudo apachectl stop $ # Temporarily disable SSL in your httpd.conf. $ sudo sed -i -e 's/LoadModule ssl_module/#LoadModule ssl_module/g' /etc/apache2/httpd.conf $ # Create empty certificate files to placate "apachectl configtest" below. $ # These empty certificate files won't be used, and will be overwritten $ # by certbot later with the real copies. $ sudo mkdir -p /etc/letsencrypt/live/cyclecounters.org $ sudo touch /etc/letsencrypt/live/cyclecounters.org/cert.pem $ sudo touch /etc/letsencrypt/live/cyclecounters.org/fullchain.pem $ sudo touch /etc/letsencrypt/live/cyclecounters.org/privkey.pem $ # If you don't get "Syntax OK", don't even bother trying the rest. $ # Instead, fix the problems exposed by configtest first. $ sudo apachectl configtest Syntax OK $ sudo apachectl start $ # This creates a single certificate file valid for all six domain names. $ # You could also create separate certificates for each, it's up to you. $ # If you have a lot of subdomains, combining them into fewer certificates $ # makes it easier to avoid the letsencrypt rate limits. $ sudo certbot certonly --webroot --rsa-key-size 4096 \ -w /Users/adam/sites/main \ -d cyclecounters.org \ -d www.cyclecounters.org \ -w /Users/adam/sites/nara \ -d nara.cyclecounters.org \ -d www.nara.cyclecounters.org \ -w /Users/adam/sites/hilohigh \ -d hilohigh.cyclecounters.org \ -d www.hilohigh.cyclecounters.org A bunch of certbot output, hopefully ending in: IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/cyclecounters.org/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/cyclecounters.org/privkey.pem Your cert will expire on 2019-02-03. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le $ # Stop the currently running web server $ sudo apachectl stop $ # Now re-enable SSL since the certificates have been created $ sudo sed -i -e 's/#LoadModule ssl_module/LoadModule ssl_module/g' /etc/apache2/httpd.conf # # Restart apache, and you should be up and running with SSL! $ sudo apachectl start
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs\ /PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>org.cyclecounters.renewcerts</string> <key>ProgramArguments</key> <array> <string>sudo</string> <string>/usr/local/bin/certbot</string> <string>renew</string> </array> <key>StartInterval</key> <integer>47000</integer> <key>RunAtLoad</key> <true/> <key>StandardErrorPath</key> <string>/dev/null</string> <key>StandardOutPath</key> <string>/dev/null</string> </dict> </plist>Of course, your version should call the file something different, like com.yourdomain.renewcerts.plist, and the Label key should use that name, too. Then tell launchd to run your program:
$ # You must set the permissions on your .plist file for launchctl to use it. $ chmod 600 ~/Library/LaunchAgents/org.cyclecounters.renewcerts.plist $ sudo chown root:wheel ~/Library/LaunchAgents/org.cyclecounters.renewcerts.plist $ # This will only work while you are logged in to the desktop. $ # There are other directories you can use that will run things all the $ # time, cron-style, but I couldn't write to them without disabling $ # System Integrity Protection, which in turn is tricky to do when you cannot $ # easily boot your remotely co-located Mac into recovery mode. Sigh. $ launchctl load ~/Library/LaunchAgents/org.cyclecounters.renewcerts.plist $ # This shows that the command ran successfully. It'll run every 13 hours $ # and change -- the 47,000 seconds figure in the .plist -- which will prevent $ # it from hammering the letsencrypt servers at any specific time. $ launchctl list | grep cyclecounters - 0 org.cyclecounters.renewcerts $ date Sun Nov 4 21:45:44 HST 2018 $ # Here we can see that the last renewal attempt worked fine. $ sudo tail -n 1 /var/log/letsencrypt/letsencrypt.log 2018-11-04 21:43:40,079:DEBUG:certbot.renewal:no renewal failures $