SSL certificate maximum validity is being reduced to 200 days from March 2026. Read more →

macOS SSL certificate with Lego and LaunchDaemon

macOS is rarely a public web server, but when it is, Lego plus a LaunchDaemon works beautifully. The whole setup is one Go binary plus two scripts, and it works on both Intel and Apple Silicon. We use Lego v5.0.4 or later for ARI support.

Setup

Step 1: Install Lego

The following command fetches the latest Lego release, detects the architecture and installs the binary to /usr/local/bin/lego. It also creates the Lego storage folders.

sudo mkdir -p /usr/local/bin
arch=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')
version=$(curl -fsSL https://api.github.com/repos/go-acme/lego/releases/latest \
  | sed -n 's/.*"tag_name": "\(v[^"]*\)".*/\1/p' | head -1)
curl -fL "https://github.com/go-acme/lego/releases/download/${version}/lego_${version}_darwin_${arch}.tar.gz" -o lego.tar.gz
tar -xzf lego.tar.gz lego
sudo install -m 0755 lego /usr/local/bin/lego
rm -f lego lego.tar.gz
sudo install -d -m 0755 /usr/local/etc/lego
sudo install -d -m 0700 /usr/local/etc/lego/accounts /usr/local/etc/lego/certificates

Step 2: Create the sync script

Lego runs this script via --deploy-hook after every successful issuance or renewal. Adjust the 4 variables at the top to match your environment. The restart command can be sudo nginx -s reload, sudo apachectl -k graceful, or something more specific to whatever service you run.

sudo tee /usr/local/bin/fairssl-lego-sync.sh > /dev/null << 'SCRIPT'
#!/bin/bash
set -euo pipefail
TARGET="/usr/local/etc/ssl"
CERT_FILE="cert.pem"
KEY_FILE="key.pem"
RESTART_CMD="sudo nginx -s reload"

lego_name="$${1:-}"
cert_path="$${LEGO_HOOK_CERT_PATH:-}"
key_path="$${LEGO_HOOK_CERT_KEY_PATH:-}"
if { [ -z "$cert_path" ] || [ -z "$key_path" ]; } && [ -n "$lego_name" ]; then
    cert_path="/usr/local/etc/lego/certificates/${lego_name}.crt"
    key_path="/usr/local/etc/lego/certificates/${lego_name}.key"
fi
if [ -z "$cert_path" ] || [ -z "$key_path" ] || [ ! -s "$cert_path" ] || [ ! -s "$key_path" ]; then
    echo "Missing Lego certificate files. Upgrade to Lego v5.0.4+ and check /usr/local/etc/lego/certificates."
    exit 1
fi
sudo install -d -m 0755 "$TARGET"
sudo install -m 0644 "$cert_path" "$TARGET/$CERT_FILE"
sudo install -m 0600 "$key_path" "$TARGET/$KEY_FILE"
$RESTART_CMD
SCRIPT
sudo chmod +x /usr/local/bin/fairssl-lego-sync.sh

Step 3: Issue the certificate

Make sure the domain is configured for FairSSL AutoDNS. Grab the EAB keys from the ACME tab in your FairSSL control panel and run Lego.

sudo /usr/local/bin/lego run \
  --domains "www.example.com" \
  --server https://fairssl.dk/acme \
  --eab --eab.kid YOUR_EAB_KID --eab.hmac YOUR_EAB_HMAC \
  --accept-tos --email acme-client@fairssl.dk \
  --http --http.webroot "/Library/WebServer/Documents" \
  --path "/usr/local/etc/lego" \
  --deploy-hook "/usr/local/bin/fairssl-lego-sync.sh www-example-com"

Step 4: Schedule renewal with LaunchDaemon

macOS uses LaunchDaemons rather than cron. Drop in this plist and Lego will run daily at a randomised time. ARI decides when an actual renewal happens; --renew-days 7 is the fallback.

sudo tee /Library/LaunchDaemons/com.fairssl.lego-www-example-com.plist > /dev/null << 'EOF'
<?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>com.fairssl.lego-www-example-com</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>-c</string>
        <string>/usr/local/bin/lego run --domains "www.example.com" --server https://fairssl.dk/acme --accept-tos --email acme-client@fairssl.dk --http --http.webroot "/Library/WebServer/Documents" --path "/usr/local/etc/lego" --renew-days 7 --deploy-hook "/usr/local/bin/fairssl-lego-sync.sh www-example-com"</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>4</integer>
        <key>Minute</key>
        <integer>17</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>/var/log/lego.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/lego.err</string>
    <key>RunAtLoad</key>
    <false/>
</dict>
</plist>
EOF
sudo chown root:wheel /Library/LaunchDaemons/com.fairssl.lego-www-example-com.plist
sudo chmod 644 /Library/LaunchDaemons/com.fairssl.lego-www-example-com.plist
sudo launchctl load -w /Library/LaunchDaemons/com.fairssl.lego-www-example-com.plist

Step 5: Verify and debug

  • Test the LaunchDaemon manually: sudo launchctl start com.fairssl.lego-www-example-com
  • Check logs: tail -f /var/log/lego.log /var/log/lego.err
  • Verify the certificate with FairSSL\'s SSL scanner.
  • To debug the ACME flow itself, prefix the command with LEGO_DEBUG_ACME_HTTP_CLIENT=true.

Frequently asked questions

Find answers to the most common questions about SSL certificates and FairSSL.

Lego is a single Go binary with no dependencies, which suits macOS installs where you do not want Python or Homebrew managing the certificate client. Lego supports ARI (RFC 9773), EAB and a broad list of DNS providers. Certbot also works, but pulls in Python and a wider dependency stack.
Yes. macOS has moved away from cron. launchd via a LaunchDaemon is the recommended way to schedule background jobs that need to run regardless of user session. Our example installs a LaunchDaemon in /Library/LaunchDaemons that runs Lego daily at a randomised time.
Yes. The install script auto-detects the architecture (uname -m) and downloads the correct Lego binary. Lego supports both amd64 (Intel) and arm64 (Apple Silicon).
A separate sync script (fairssl-lego-sync.sh) runs via Lego's --deploy-hook after every successful issuance or renewal. Adjust the script with the path your service expects and a restart command. The script ships example restart commands for nginx, Apache and generic services.
From Lego v5 the renew command is gone. lego run checks ARI and only renews if needed. You can force renewal with --renew-days 7, which our example uses as a fallback if ARI does not respond.
Only for the first run. Lego stores the ACME account locally under /usr/local/etc/lego/accounts, and future renewals reuse it without EAB. Keep the keys somewhere safe as a backup in case you ever need to rebuild the setup.

Ready to automate macOS certificates?

Create a free account and issue your first certificate in under 10 minutes.