Update: As of macOS 10.14 Mojave, macOS Server has been gutted and no longer include websites. This guide only applies to versions of macOS Server prior to macOS 10.14 Mojave.
Apple’s Server app can make basic sysadmin functions really simple for a novice webmaster. It can also make managing multiple websites, specifically multiple SSL websites, a nightmare.
I’m a firm believer that everyone should be using HTTPS for all their websites even if they do not use eCommerce. You don’t see me selling anything on this website (at least, not at the time of this post), but yet you’ll find that your browser is displaying a lock next to “itim.co” up above. It’s just the smart, safe thing to do, and thanks to the folks at Let’s Encrypt, you can have all the safety and security that comes with HTTPS for $0.00.
This isn’t the place to debate the merits of using macOS Server over virtually any Linux distro or Windows Server. This is for the green sysadmin-in-training with a Mac and nothing else who just wants a secure website (or four).
For the rest of this post, I’m going on the assumption that you know how to use the command line, but are relatively new at managing a macOS Server. We’re going to be issuing certificates for a fictional website, foobar.co.
Let’s Talk Let’s Encrypt and macOS Server
Let’s Encrypt is a California-based non-profit certificate authority (CA) trusted by every major browser in existence. They’re backed by Mozilla, Cisco, Chrome, the Internet Society, and a plethora of organizations that have credibility across the interwebs. They will issue you a free TLS/SSL certificate for your website or websites at absolutely no cost. The only catch is that the certificates are only valid for 90 days, so they encourage you automate your renewal process. More on that at a later date (aka, once I figure it out).
If you’re here, chances are you found lots of tutorials on getting Let’s Encrypt certs working on Linux or Windows, but few (or none) for macOS. Never fear. I got you fam.
First, you’ll need to install HomeBrew.
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”
HomeBrew is an incredibly useful installer for lots and lots of things and whatnots, but for now, we just want Let’s Encrypt. After the HomeBrew installer is finished…
$ brew install certbot
Certbot is the shit. It will contact the Let’s Encrypt CAs and issue our certificates for us. It has lots of options, but we just want some certificates.
Certbot must be run as a super user, so let’s go ahead and login as su.
$ sudo sh Password:
Notice your console line begins with “sh-3.2 #” instead of the usual “computer:~ user$”. Now you won’t have to type your su password again until you terminate the session.
Let’s Get Some (certificates)!
Certbot, like all CA and CA utilities, require some form of verification that you actually own the server you are requesting certificates for. It accomplishes this with two methods:
- Webroot Challenge (Automatic) – This is the best and fastest method to verify your domain. It temporarily creates a hidden folder on your website’s root directory with a hash challenge inside. Certbot then contacts an external server, who accesses that hidden directory, and confirms that the hash challenge passes.
- DNS Challenge (Manual) – I would only recommend you go this route if you can’t use the webroot challenge, such as if you do the smart thing and disable non-HTTPS websites across your entire server and you don’t yet have a valid certificate for your domain. Certbot gives you a hash answer to add as a TXT record on your DNS. Once it confirms that record exists, it gives you a passing grade.
Going forward, anything highlighted in red indicates you should change those values to match your website and configuration. Otherwise, be The Beatles and Let It Be.
On macOS Server, by default all webroot folders are stored in /Library/Server/Web/Data/Sites/ . Since I operate multiple domains, I have subdirectories /foobar-co/www/ for each website.
For our foobar.co website, the webroot challenge would look like this:
# certbot certonly --webroot -w /Library/Server/Web/Data/Sites/foobar-co/www -d foobar.co -d www.foobar.co
Let’s break that down. The first argument – certonly – tells Certbot we’re only generating a certificate (because it can do many things, remember?). The next argument is the challenge method (webroot)
-w is the full path to your webroot.
-d is the domain of this website. You can have many domains for one certificate, and you’ll likely want that. A certificate for only foobar.co is NOT valid for www.foobar.co! You have to specifically include the www. The first domain you list after the webroot is the certificate’s common name (CN), and is what will be displayed first whenever someone views your certificate on their browser.
For the DNS challenge, most of the above remains the same, except, of course, without the webroot.
# certbot --manual --preferred-challenges dns certonly -d foobar.co -d www.foobar.co
You’ll be prompted to enter the generated hash as a TXT record on your DNS. Once you’ve down that, you’ll enter ‘Y’ to proceed.
If all goes well, you’ll get a “Congratulations!” Your certificates are all done!!!1!… well, almost.
Importing the LE certificates into macOS Server
Certbot does not do this portion for you. It creates three files in the /etc/letsencrypt/live/foobar.co/ directory (that last directory is the common name):
- cert.pem – your certificate file and public key
- privkey.pem – your oh-so-very-important-and-secretive private key
- chain.pem – the full trust chain needed to link your certificate to the already trusted Let’s Encrypt CA, and to their CA, DST Root CA.
Sure, you could manually drag these files into the Server app, but the goal is to make this process as streamlined as possible, because we have to either automate it or do it every 90 days. To import a complete certificate in the Server app, we have to package it first.
openssl pkcs12 -export -inkey /etc/letsencrypt/live/foobar.co/privkey.pem -in /etc/letsencrypt/live/foobar.co/cert.pem -certfile /etc/letsencrypt/live/foobar.co/chain.pem -out /etc/letsencrypt/live/foobar.co/foobar.co.p12 -passout pass:**ASTRONGPASSWORD**
This packages up all those files into a secure PKCS12 file, encrypted with a password (don’t include the asterisks). Now to get that into the Server app…
security import /etc/letsencrypt/live/foobar.co/foobar.co.p12 -f pkcs12 -k /Library/Keychains/System.keychain -T /Applications/Server.app/Contents/ServerRoot/System/Library/CoreServices/ServerManagerDaemon.bundle/Contents/MacOS/servermgrd -P**ASTRONGPASSWORD**
(Note that unlike the OpenSSL command, the password isn’t prefaced with a “pass:”, but rather with a “-P” followed immediately by your decryption password, no space.)
Voila!! Go look at the Certificate tab of the Server app. You can now assign your certificate to the foobar.co website! Neat, huh?
That’s great and all, but what if I have multiple websites needing certificates?
This problem plagued me for years. For reasons unknown to all mankind not working in Cupertino, if you have multiple certificates for multiple websites in the Server app, all websites will use the certificate belonging to the first website in your list.
So if you have three websites…
– foobar.co
– antitrumpet.com
– betabet.xyz
… and you generate your certificates and assign them to their corresponding websites in the Server app like so…
– foobar.co – cert foobar.co
– antitrumpet.com – cert antitrumpet.com
– betabet.xyz – cert betabet.xyz
… what your visitors see is this:
– foobar.co – cert foobar.co
– antitrumpet.com – cert foobar.co
– betabet.xyz – cert foobar.co
This is obviously a major bug, and has existed since at least Mac OS X 10.9 Mavericks (I did not use Server prior to that iteration). For the last three years, the only solution I had was to generate ONE certificate for every website by including each webroot and domain I had.
# certbot certonly --webroot -w /Library/Server/Web/Data/Sites/foobar-co/www -d foobar.co -d www.foobar.co -w /Library/Server/Web/Data/Sites/antitrumpet-com/www -d antitrumpet.com -d www.antitrumpet.com -w /Library/Server/Web/Data/Sites/antitrumpet-com/www -d antitrumpet.com -d www.antitrumpet.com -w /Library/Server/Web/Data/Sites/betabet-xyz/www -d betabet.xyz -d www.betabet.xyz
I don’t recommend this for two reasons:
- It’s not a good practice to include multiple fully-qualified domain names on one certificate.
- If you ever add a website, you have to regenerate a whole new certificate by expanding that command above.
However, there is a better way.
Remember when I said that the reasons for this were unknown to all mankind not working in Cupertino? I lied. I know.
You see, the Server app expands beyond a lot of basic web server functionality and encompasses far more than just websites. It includes tools like Profile Manager, a great MDM (mobile device management) solution for organizations of practically any size. That service is web-based, and, if turned on, is accessible via foobar.co/profilemanager. But there’s the problem. Since it’s web-based and uses (a copy of) the web server, there’s a port conflict on ports 80 (HTTP) and 443 (HTTPS). To get around this conflict, Apple set up a serious of proxies in all the web server configuration files (.conf) that reroute traffic looking for your websites on ports 80 and 443. All “website” traffic is diverted to port 34580 for HTTP, and 34543 for HTTPS, while Profile Manager and other web-based services remain on ports 80 and 443.
I know, right?
tl;dr, in the Server app, HTTPS traffic is secretly on port 34543, not 443.
This seems completely unnecessary and convoluted. If anything, they should have rerouted Profile Manager et. al. to those alternate ports and kept web traffic on 80 and 443, because that’s the portion people are going to customize the most. Keep it standardized!
So how do we fix the certificate issue? It’s actually a very simple solution. You declare a wildcard NameVirtualHost on port 34543. You need to edit the primary web server configuration file /Library/Server/Web/Config/apache2/httpd_server_app.conf
$ vi /Library/Server/Web/Config/apache2/httpd_server_app.conf
Scroll down alllllll the way to the end. Right below “RewriteEngine On”, add the (red) following:
. . .
RewriteEngine On
NameVirtualHost *:34543
. . .
To save and exit vi editor, enter the following key sequence:
[ESC] [:] [w] [q] [RETURN]
Now restart the Web services, and all your websites should now pull their appropriate, newly created Let’s Encrypt certificates.
You made! What a champ. Now instead of rethinking your life for choosing macOS Server, you can sit back and enjoy your multiple secured-for-free websites.
This was very good information it helped me resolve and issue having one certificate for two domain names.
The web sites I’m working on are working however, I get this message in the /var/syslog:
May 29 00:00:09 webserver com.apple.xpc.launchd[1] (org.apache.httpd[3297]): Service exited with abnormal code: 1
May 29 00:00:09 webserver com.apple.xpc.launchd[1] (org.apache.httpd): Service only ran for 0 seconds. Pushing respawn out by 10 seconds.
I know this is related to the MacOS System Integrity Protection. I’ve tried setting the csrutil disable in recover mode reboot restart web services and then doing a csrutil enable again in in a recover mode boot and rebooting again. The issue come back.
Honestly, at this point you’re better off standing up an Ubuntu virtual machine. macOS Server is long dead, now.