Let’s Encrypt….
Let’s encrypt, why not?
Wanna know how I did it for FreeBSD/Apache/acme-client, jump below.
Let’s encrypt is a service from the fine people at Mozilla, who, when they’re not trying to prove that Firefox can be a Chrome clone, do some really good stuff. Certificates are what give you the little warm fuzzy feeling of a green lock icon, and when properly configured, avoid giving you that terrifying feeling that something horrible is about to happen if you visit a site with an expired one or a self-signed one.
There are some huge structural problems in the certificate concept that seem to exist only to validate the certificate mafia, that can charge $100s per year for a validated certificate, as if executing the script to issue one was somehow expensive. It is not, you can generate one yourself that provides exactly the same security as one provided by a big company that gets their root certs distributed in a browser, but browsers reject these with scary messages so webmasters have to keep buying them.
Now there’s a theory behind why they’re ripping you off: the premise is that the certificate verifies the site is who it says it is – that if you go to mybank.com, you’re actually visiting your real bank, not being redirected by a man-in-the-middle attack to some fake landing page to harvest your passwords, log into your account, and steal all your cats. There are a few problems with this:
- Nobody actually checks a URL so while a certificate sort of adds some weight to the probability that mybank.com is owned by mybank, not some hacker a few tables over ARP poisoning the cafe wifi, it doesn’t do anything if you click on a link to mibank.com.
- The companies that claim to check IDs and verify owners, do not. That would cost money. You think they’re gonna actually do that? No… (CAcert actually does, but they don’t get a root cert because… they do it for free. And don’t have Mozilla’s money and clout.)
- Stealing a root cert private key can generate significant LOLZ; it happens a lot.
- Law enforcement the world over has “lawful intercept” certs. You’re probably on some country’s poop list if you have ever used social media. Their laws permit intercepting your communications. Some country’s laws somewhere certainly do no matter who you are.
- But dang, those annoying warnings that do nothing to secure you mean that people who publish a website just for the good of the planet either have to pay up, go through a lot of hassle, or leave their user’s content streams exposed to the world’s prying eyes…
…Until Let’s Encrypt came along. It is a lovely little set of tools and services that not only issue browser-accepted certs (see the green lock?) but also automate renewal. They basically check that you have enough control over your website to let a script write a file that that they can read back and verify, and if so, you’re who you say you are: the person with write access to the server powering the website they’re giving the certificate too. That’s all anyone can really do, and is as secure as any other cert there is for identification of a site: that is except for stolen certs, url typos, law enforcement certs, or malicious code on your computer, if you visit https://brt.llc/ and you don’t get any warnings, you’re probably reading data coming off my computer and not some hacker pretending to be me.
I got Let’s Encrypt to work, but it took some modifications of the existing guides, and I think the service is a good thing that more people should use, so in the spirit of investing some of my resources into the great shared experiment that is Open Source, here’s my How To:
Upstream Guides:
I found these two guides extremely helpful.
https://brnrd.eu/security/2016-12-30/acme-client.html
Step 1: Installing the certificate generation tool
There are a few different software tools to manage the Let’sEncrypt process. I elected to use Kristaps Dzonsons acme-client, ported to FreeBSD by Bernard Spil.
I was using OpenSSL on my site. Bernard and Kristaps have some strong opinions on OpenSSL and heartbleed and a few other problems and therefore require LibreSSL. If you’re using it already, great. If not, you’ll have to install it. It wasn’t too terrible, but I ran into a few issues:
https://wiki.freebsd.org/LibreSSL
Or, easy peasy https://ootput.github.io/2016/07/20/Switching-to-LibreSSL/
ee /etc/make.conf -> set DEFAULT_VERSIONS+= ssl=libressl portmaster -od security/libressl security/openssl portmaster -rd security/libressl
if that fails with
===>>> The argument to -r must be a package name, or a glob pattern
Then try:
pkg version -v | grep libre libressl-2.6.3 = up-to-date with index portmaster -rd libressl-2.6.3 # or for a complete refresh portmaster -Rafd
Curl will probably fail with LibreSSL (and with the latest, if it has brotli support enabled). Check the google to see if these fixes are still needed, or just:
# cd /usr/ports/ftp/curl # make config
disable TLS-SRP https://forums.freebsd.org/threads/56917/
ftp/curl 7.75.0 has an issue with pied piper brotli, which requires modifying the makefile
to build --without-brotli
as indicated in comment #2
(Sunpoet, the curl port maintainer, got back to me with an update: when PR/223966 is integrated in Brotli, he will add an optional Brotli support flag and it should work fine at that point without the Makefile
edit.)
Step 2: Actually installing acme-client
The really easy part: you should be able to
# portmaster security/acme-client
and be on your way to configuration heaven.
Step 3: Initial configuration
The defaults for acme-client expect certain directories to exist and the installer doesn’t create them.
# mkdir -pm750 /usr/local/www/.well-known && chown -R www:www /usr/local/www/.well-known # mkdir -pm750 /usr/local/www/.well-known/acme-challenge && chown -R www:www /usr/local/www/.well-known/acme-challenge
The how-to’s seemed to forget the last one.
And make a modification to your httpd.conf file to permit the Let’s Encrypt servers to have access to these folders:
# ee /usr/local/etc/apache24/httpd.conf
add the following:
# Lets Encrypt challenge directory configured per # https://brnrd.eu/security/2016-12-30/acme-client.html <Directory "/usr/local/www/.well-known/"> Options None AllowOverride None Require all granted Header add Content-Type text/plain </Directory>
And, for each VHOST that is going to get a cert:
# ee /usr/local/etc/apache24/extra/httpd-vhosts.conf
add to each non-ssl VHOST definition the following:
Alias /.well-known/ /usr/local/www/.well-known/
such that you end up with something like (yours may be different, especially watch out for BasicAuth or ModRewrite, addressed further down):
<VirtualHost IP.NU.MB.ER:80> ServerName domain.com ServerAdmin admin@domain.com DocumentRoot /usr/local/www/data-dist/domain-root ServerAlias *.domain.com www.domain.com Alias /.well-known/ /usr/local/www/.well-known/ ErrorLog /var/log/domain-error_log CustomLog /var/log/domain-access_log combined ScriptAlias /cgi-prg /www/cgi-prg </VirtualHost>
Don’t forget!
# apachectl restart
Step 4: First Try
At this point the system should be configured sufficiently to do a trial run with a single domain from the command line. Later on there are some scripts that will automate the process of both converting a large number of VHOSTed domains on a server to Let’s Encrypt and for maintaining them and getting email notifications if anything goes wrong in the, hopefully, fully automatic renewal process.
acme-client -mvnNC /usr/local/www/.well-known/acme-challenge domain.com www.domain.com
This should create all the directories still needed and populate them, then check with the lets encrypt server and get a certificate and install it in the right place. Inshalla.
If you get something like
acme-client: transfer buffer: [{ "type": "urn:acme:error:malformed","detail": "Provided agreement URL[https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf]does not match current agreement URL[https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf]","status": 400 }] (267 bytes)
That means the lets encrypt agreement has changed. You can’t do much but write the port maintainer or wait for an update. It will get fixed quickly and should only happen once a year. I don’t think you’ll get it at all unless you’re unlucky enough to try to update when it is changing. I was.
More likely you’ll get something like
acme-client: transfer buffer: [{ "type": "http-01", "status": "invalid", "error": { "type": "urn:acme:error:unauthorized", "detail": "Invalid response from : (etc...)
This means there’s a problem accessing the /.well-known/ directory by the server. There can be a lot of reasons for this:
- You didn’t restart apache
# apachectl restart
- There was an error in the config file (look at the output of the restart) and therefore apache didn’t actually reaload with your new config.
- DNS isn’t pointing where you think it is pointing. Check with
nslookup/whois
to make sure. Really. - You have the directories protected in some way – like with
.htaccess
. (see below)
But if it goes well, you’ll get something like:
acme-client: /usr/local/etc/acme/domain.com/privkey.pem: account key exists (not creating) acme-client: /usr/local/etc/ssl/acme/private/domain.com/privkey.pem: domain key exists (not creating) acme-client: : directories acme-client: acme-v01.api.letsencrypt.org: DNS: 173.223.13.221 acme-client: acme-v01.api.letsencrypt.org: DNS: 2001:418:142b:290::3d5 acme-client: acme-v01.api.letsencrypt.org: DNS: 2001:418:142b:28d::3d5 acme-client: : req-auth: domain.com acme-client: /usr/local/www/.well-known/acme-challenge/_ffVe6jHNHbIG1XKAeoqQmmtryWMGCKsfHIWWkl5lJw: created acme-client: : challenge acme-client: : status acme-client: : certificate acme-client: http://cert.int-x3.letsencrypt.org/: full chain acme-client: cert.int-x3.letsencrypt.org: DNS: 184.23.159.176 acme-client: cert.int-x3.letsencrypt.org: DNS: 184.23.159.177 acme-client: cert.int-x3.letsencrypt.org: DNS: 2001:5a8:100::b817:9fb0 acme-client: cert.int-x3.letsencrypt.org: DNS: 2001:5a8:100::b817:9fb1 acme-client: /usr/local/etc/ssl/acme/domain.com/chain.pem: created acme-client: /usr/local/etc/ssl/acme/domain.com/cert.pem: created
Yay, you’ve got certs! Now update your vhosts file to point to the certs you just created. You may need to add a 443 container or, if it exists, update it to point to the new certs and restart apache.
# ee /usr/local/etc/apache24/extra/httpd-vhosts.conf
<VirtualHost IP.NU.MB.ER:443> ServerName domain.com ServerAdmin admin@domain.com DocumentRoot /usr/local/www/domainroot ServerAlias domain.com sub.domain.com SSLCertificateFile /usr/local/etc/ssl/acme/domain.com/cert.pem SSLCertificateKeyFile /usr/local/etc/ssl/acme/private/domain.com/privkey.pem SSLCertificateChainFile /usr/local/etc/ssl/acme/domain.com/fullchain.pem Header set Strict-Transport-Security "max-age=31536000; includeSubDomains" ErrorLog /var/log/domain-error_log CustomLog /var/log/domain-access_log combined </VirtualHost>
save and restart, look for any errors (typos on directory paths etc. will be detected and apache won’t restart, but be aware, it won’t quit either).
# apachectl restart Performing sanity check on apache24 configuration: Syntax OK Stopping apache24. Waiting for PIDS: 81160. Performing sanity check on apache24 configuration: Syntax OK Starting apache24.
Navigate to https://www.domain.com/ and check out your new green lock. Check security and you should find:
W00T!
Acme-Client Options
# man acme-client
has all the deets, but we’re using:
- -m to append the domain name to paths, use this and always use it or never.
- -v for verbose output so we can see what is going on.
- -n to check if an account key exists and create if not (no reason to omit)
- -N to check if a domain key exists and create if not (also no reason to omit)
- -C to specify the path to the challenge dir. These guides all assume a centralized challenge dir outside the main serving path, and to which we redirect via an
alias
directive. - -F which forces the recreation of certs even if they haven’t expired (this counts against your 10 per 3 hours limit)
- -s which redirects the process to the Let’s Encrypt staging server, which has no volume limits but also doesn’t create certs browsers accept. (Using this is fine, but requires cleanup to switch to the production server, see below)
- -e which is used to add a SAN to the certificate. Removing one is a bit more involved (see below).
Automating Registration
Lets say you have a lot of domains, you might want to automate the process. I modified the renewal script to automate the registration process. This saved some time, but one quirk is you can only register 10 domains (certificates, including SANs, basically 10 lines of the domains list) per 3 hours (they say-I found it takes more like 12 hours to be allowed to register more).
First create a file with all the domains you want to register for a Let’s Encrypt certificate in the same format as the renewal script uses (it can be the same file, but I made it different as I was experimenting)
# ee /usr/local/etc/acme/newdomains.txt domain.com www.domain.com domain2.com www.domain2.com domain3.com www.domain3.com (save) # ee /usr/local/etc/acme/acme-client-bulk-add.sh
#!/bin/sh ### # # This script was adapted by Richard Fassett from letskencrypt.sh # by Bernard Spil # See https://brnrd.eu/security/2016-12-30/acme-client.html # # and updated again from richard fassett's script at # https://web.archive.org/web/20180813162024/https://www.richardfassett.com/2017/01/16/using-lets-encrypt-with-acme-client-on-a-freebsd-11apache-2-4/ # # this requires a file called /usr/local/etc/acme/newdomains.txt of the format # domain.tld sub.domain.tld alt.domain.tld # domain2.tld # domaind3.tld sub.domain3.tld # etc # # This should only be run to bulk-add domains. ### # Define location of dirs and files DOMAINSFILE="/usr/local/etc/acme/newdomains.txt" CHALLENGEDIR="/usr/local/www/.well-known/acme-challenge" # Loop through the newdomains.txt file with lines like # example.org www.example.org img.example.org cat ${DOMAINSFILE} | while read domain subdomains ; do # Create the cert directory with the command # acme-client -mvnNC /usr/local/www/.well-known/acme-challenge (domain subdomains) acme-client -mvnN -C "${CHALLENGEDIR}" ${domain} ${subdomains} done # chmod +x /usr/local/etc/acme/acme-client-bulk-add.sh
A few fixes/recoveries that might be useful at this point: add SAN, remove SAN, switch from staging to production Let’s Encrypt servers.
Automation can break things, you might find you adjusted a few domains incorrectly or want to add a SAN later.
If you need to redo a domain from scratch, for example if you use the “s” option which created a cert from the staging server that doesn’t have volume limits (maybe you’re testing a lot of domains or trying to debug a particularly tricky .htaccess or DNS condition) – you might create a domain with acme-client -mvnsNC /usr/local/www/.well-known/acme-challenge domain.com www.domain.com
and then want to generate the production cert. You also need to do this to remove a SAN. If you try without deleting the directories, you’ll get something like unknown SAN entry.
(You replace “domain.com” with your domain.)
# setenv DD domain.com # rm -r /usr/local/etc/ssl/acme/private/$DD && rm -r /usr/local/etc/acme/$DD && rm -r /usr/local/etc/ssl/acme/$DD && acme-client -mvnFNC /usr/local/www/.well-known/acme-challenge $DD www.$DD
If you need to add a new SAN to an existing domain
acme-client -mvneFNC /usr/local/www/.well-known/acme-challenge domain.com www.domain.com newsub.domain.com
it is the -e
that “extends” the certificate.
Step 5: Automating Renewal
You might notice that the duration of the certificate is rather short: 3 months. You really don’t want to be responding to certificate expired errors every 3 months, so let’s automate the renewal process. For this you can create two files and store them on your server. One is the renewal script itself and the other is a list of domains to renew. This assumes you have more than one domain. If you only have one domain, this is a bit overkill, but it will work, so why not? You might get more domains in the future. Everyone does.
First create a file with your list of domains, call it something creative like “domains.txt” This is really a certificate request list with the “primary” domain and Subject Alternative Names (SANs) each on a single line. In theory the SANs can be all over the place and Let’s Encrypt allows up to 100 per certificate (quite a lot), so the implication of “domains.txt” naming is a bit inaccurate, but that’s what everyone is using so we won’t be contrary. You have to make sure that all the subdomains resolve—the Let’s Encrypt servers are going to look them up via DNS and if there aren’t working entries, this will fail with one of the errors above. Check first. I have not tested whether, if for example, you own domain.com, domain.org, and domain.net and they all point to the same directory, you can use one cert with different TLDs (or domains) as SANs; you should be able to, but I didn’t try.
# ee /usr/local/etc/acme/domains.txt domain.com www.domain.com sub.domain.com sub2.domain.com domain.org www.domain.org domain2.com www.domain2.com cats.domain2.com kittens.domain2.com
Now that you’ve saved that, the following script is adapted from a few at the references listed above and works on my server. I made a few adjustments and corrections (there was a name change for acme-client which hasn’t quite propagated through all the HowTos yet).
# ee /usr/local/etc/acme/acme-client-update.sh
#!/bin/sh ### # # This script was adapted from letskencrypt.sh by Bernard Spil # See https://brnrd.eu/security/2016-12-30/acme-client.html # ... and further modified by David Gessel # This script will fail if the directories haven't been set up and # domains in domain.txt have been successfully verified # ### # Define location of dirs and files DOMAINSFILE="/usr/local/etc/acme/domains.txt" CHALLENGEDIR="/usr/local/www/.well-known/acme-challenge" # is changed to 1 if any domains expired and were renewed CHECKEXPIRATION=0 # Loop through the domains.txt file with lines like # example.org www.example.org img.example.org cat ${DOMAINSFILE} | while read domain subdomains ; do # acme-client returns RC=2 when certificates # weren't changed; use set +e to capture the return code set +e # Renew the key and certs if required acme-client -mvb -C "${CHALLENGEDIR}" ${domain} ${subdomains} RC=$? # now that we have the return code, set script to exit if # nonzero is returned set -e # if anything is expired, we'll want to do something # (e.g., restart HTTPS) if [ $RC -ne 2 ] ; then CHECKEXPIRATION=1 fi done if [ "$CHECKEXPIRATION" -ne "0" ] ; then service apache24 restart fi # chmod +x /usr/local/etc/acme/acme-client-update.sh
This works quite well and will walk through your domains and renew as needed.
I have 36 domain/certificate lines in my “domains.txt” file and timing this script it takes 2.13 seconds to execute on my server. There’s no real problem running it every night and if you have a lot of domains, you should remember you can only get 10 certs at a time and they won’t renew for about a week before expiry, a limitation I ran into in the bulk setup process. You can spread your domain renewals out over the three months by force renewing blocks of them if you have more than about 60 per server.
You probably want to automate the process as a cron job. But before we do, lets address one more little problem: one of the shortcomings of the script process below is that the output messages of the script are output to stdout
and only cron’s stderr
is emailed to the admin. If your shell environment is wrong or the path to the script is wrong, cron will tell you, but if your domains don’t resolve or the script can’t reach /.well-known/
, you will not get any warnings. That’s might be a bummer. So I redirect the output of the client-update.sh script to a log file. It gets overwritten with each execution, so it doesn’t need to be rotated – it is just the output of the last execution. It should be filled with lines including “adding SAN
” (which it tells you for each domain) and “certificate valid
” which it tells you for each cert that doesn’t need to be renewed. But it might tell you something else, like it barfed trying to reach the /.well-known/
directory because, say, you messed around with .htaccess
or forgot to renew your domain and it is being redirected to parking or something. The following script first checks to see if there are any lines in /var/log/lets-encrypt-renew
other than the expected, and if so, emails just those lines. You shouldn’t get anything until renewal time or if there’s an error. If you don’t care about renewal notices, you can edit the script to ignore those too.
# ee /usr/local/etc/acme/acme-client-errors.sh
#!/bin/sh ### # this script scans the log file created by the renewal execution cron job # then removes any lines containing "adding SAN" or "certificate valid", which # are normal messages, and mails whatever is left over using the "mail" command # check full paths (or use relative) but full paths can avoid some errors # use "# which grep" and "# which mail" on your system to check. PROBLEM=0 /usr/bin/grep -v "adding SAN" /var/log/lets-encrypt-renew | \ /usr/bin/grep -v "certificate valid" | /usr/bin/cat | \ { while read status do PROBLEM=1 done if [ "$PROBLEM" -ne "0" ] ; then /usr/bin/grep -v "adding SAN" /var/log/lets-encrypt-renew | \ /usr/bin/grep -v "certificate valid" | \ /usr/bin/mail -s "Lets Encrypt Errors" gessel@blackrosetech.com $1 fi } # chmod +x /usr/local/etc/acme/acme-client-errors.sh
My cron
configuration is set up as
# crontab -e #* * * * * command to be executed #- - - - - #| | | | | #| | | | +----- day of week (0 - 6) (Sunday=0) #| | | +------- month (1 - 12) #| | +--------- day of month (1 - 31) #| +----------- hour (0 - 23) #+------------- min (0 - 59) MAILTO=gessel # expanded path PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin SHELL=/bin/csh # Let's Encrypt renewal check * 3 * * * /usr/local/etc/acme/acme-client-update.sh > & /var/log/lets-encrypt-renew * 4 * * * /usr/local/etc/acme/acme-client-errors.sh
Note that this requires that mail works. On servers that aren’t serving email, I use SSMTP and configured it more or less following this guide https://www.freebsd.org/doc/handbook/outgoing-only.html andhttps://www.davd.eu/freebsd-send-mails-over-an-external-smtp-server/ and this https://www.debarbora.com/freebsd-10-1-setup-ssmtp-for-outgoing-mail/ especially the tip about using # chpass to change the default Full Name for root from “Charlie &” to something useful like “ServerName Root.”
You can test the mail function by adding a random word (or domain) to your domains.txt file and then executing
# /usr/local/etc/acme/acme-client-update.sh > & /var/log/lets-encrypt-renew # /usr/local/etc/acme/acme-client-errors.sh
If everything is set up right, you’ll get an email complaining about your random word not being valid. If you restore the correct domains.txt file and execute the above two commands you should not get an email at all.
# more /var/log/lets-encrypt-renew
should show only lines with “adding SAN
” and “certificate valid
” in them. If you execute # /usr/local/etc/acme/acme-client-errors.sh
you shouldn’t get any message.
.htaccess Problems
If you’re controlling access to a directory or have some non-HTML style process listening, you might run into challenges giving the Let’s Encrypt server access to the /.well-known/
directory. I found the following formulation worked:
AuthType Basic AuthName "Please login." AuthUserFile "/xxx/.htpasswd" # the directive below also "requires" that the requested URL include /.well-known/ Require expr %{REQUEST_URI} =~ m#^/.well-known/.*# Require valid-user
Basically the script above allows (requires) a “valid-user” (one with an entry in the AuthUserFile and valid matching password) and also requires (allows) a URL that is going to /.well-known/
and subdirectories thereof. This also works in /usr/local/etc/apache24/httpd.conf
and /usr/local/etc/apache24/extra/httpd-vhosts.conf
modRewrite to HTTPS problems
You can also create problems by rewriting to HTTPS. You might want to do this now that you have certs that will auto-renew and you can provide a secure experience for everyone. In order to get to the /.well-known/
directory, you have to add an exception to the mod-write rule for traffic to this subdirectory like so:
RewriteEngine on RewriteCond %{REQUEST_URI} !^/\.well\-known/acme\-challenge/ RewriteCond %{HTTPS} off RewriteRule (.*) https://%{SERVER_NAME}/$1 [R=301,L]
Also, if you redirect on a 404, some formulations cause problems. This one does not seem to:
ErrorDocument 404 /index.php