Skip to main content

A Bash Client for the ACME Protocol

These are my notes on installing acme.sh and using it on an aging Django site. They were originally written sometime in 2016 and updated in May of 2018.

A while ago, I needed to choose an ACME client, the simpler the better, when converting a few client websites to Let's Encrypt for SSL/TLS cerfificates. I really wanted bare-bones functionality, namely: get renewed certificates, when necessary. As a part of my research, I experimented Google's ACME client. One issue I had with Google's client was that it did not have support for auto-renewal.

Because my end goal was to automate certificate renewal on some fairly old legacy sites, I was particularly sensitive to any changes that a tool might make to the overall system configuration without my knowledge (e.g., updating openssl or python to newer versions, etc.). This is the paranoid OCD sysadmin part of my personality.

The Let's Encrypt certbot certainly does everything that I needed, but it seems to do a huge amount of stuff behind the scenes with python and virtualenvs. It also demands to be run as root, which scares the hell out of me. I remained skittish about simply installing and using certbot. As before, I looked for a simpler, less complicated, ACME client that would do what I needed.

There are a surprising number of acme clients out there. I finally settled on acme.sh, an ACME protocol client written purely in Shell (Unix shell) language, to manage installation and renewal of SSL certificates.

These are the steps that I followed to install, configure, and use acme.sh on a very old Django/wsgi site.

Configure httpd for http-01 Challenge

ACME supports multiple methods of confirming site ownership. I chose to use webroot method which issues an http-01 challenge. The challenge requires that a file be placed in the .well-known/acme-challenge directory of the site's root. acme.sh will do this automatically, however, one must ensure that httpd is configured to serve files from that directory.

mod_wsgi was used to serve the Django application via httpd. This meant that I could not simply allow acme.sh to blindly create the directories/files it needed in DocumentRoot. To work around this, I created an alias in httpd.conf that pointed to the static directory in which Django stores static files. This required the addition of the following line to my httpd.conf:

Alias  /.well-known/acme-challenge  /path/to/static/.well-known/acme-challenge

I also ran mkdir -p  /path/to/static/.well-known/acme-challenge to ensure that the directory path existed.

Install acme.sh

With that out of the way, it was time to install acme.sh. Note that running the install as root is optional and I chose not do so. Also, at this early point, I did not want it to automatically setup a cron job for renewal, as I was interested in seeing how well things worked before getting married.

$ git clone https://github.com/Neilpang/acme.sh.git
$ cd ./acme.sh
$ ./acme.sh --install --nocron --accountemail me@foo.com

This will install acme.sh in ~/.acme.sh. It also updates your .bashrc to source in ~/.acme.sh/acme.sh.env, which sets up an alias for acme.sh.

Unless you logout and back in, you'll need to source in the above env to perform the remaining steps.

Dryrun and Debug

LE provides a staging server that can be used to test against, prior to working with valid certificates. It's a good idea to do this so that you don't inadvertently run up against rate limits from testing.

To use the staging server (instead of the live server), supply the --staging command line option.

If you run into any problems, or if, like me, you just want to know more about what's going on, add --debug and --log to the command line options. Error messages are in red on a black background, which I found to be completely unreadable. The --no-color option helps with this.

Obtain Initial Certificates

At this point, we're ready to get the staging certificates for the site:

$ acme.sh --staging --issue -d mydomain.com -w /path/to/static

Note that the directory given to the -w option is the one that will contain the .well-known/acme-challenge directory, which we created and aliased earlier in httpd.conf.

If the above runs successfully, the newly issued certificates will be located in ~/.acme.sh/mydomain.com. Note that, at this point, they have not been installed and httpd knows nothing about them.

You can list the certificates obtained by acme.sh via:

$ acme.sh --list

At the risk of belaboring a point that is obvious to everyone, I want to summarize how the webroot mechanism works (one may rightly infer that this wasn't entirely obvious to me when I first looked at it). Specifically, for my situation as described:

  • The directory /path/to/static/.well-known/acme-challenge must exist on the filesystem.
  • Permissions must allow httpd to serve files from the directory.
  • httpd must be configured to serve files (e.g., foo) from that directory when http://mydomain.com/.well-known/acme-challenge/foo is accessed.
  • During issuance, LE will issue a crypto challenge, which acme.sh will place in this directory.
  • LE will attempt to access the challenge via HTTP. If it succeeds, the issuance process progresses. If it fails, the issuance process fails.

Install the Certificates

Your httpd.conf will contain SSL directives specifying the locations of the credentials files. My directives looked something like this:

...
SSLCertificateFile            /path/to/tls/certs/mydomain.com.crt
SSLCertificateKeyFile         /path/to/tls/certs/mydomain.com.key
SSLCertificateChainFile       /path/to/tls/certs/fullchain.crt
...

acme.sh --install-cert will install the certificates obtained in the last step in the location you specify (in my case, /path/to/tls/certs), thereby making them available for use by httpd. Because I was not running acme.sh as root, I had to ensure that uid:gid permissions were such that the files could be created/updated.

Note that the documentation warns against directly copying certificates from ~/.acme.sh. Rather, they recommend using --install-cert.

--install-cert also stores the locations into which the certificates are installed in ~/.acme.sh/mydomain.com/mydomain.com.conf. This allows those locations to be used again at renewal time.

My install-cert command looked something like this:

$ D=/path/to/tls/certs
$ acme.sh --install-cert                       \
          -d mycomain.com                      \
          --cert-file      $D/mydomain.com.crt \
          --key-file       $D/mydomain.com.key \
          --ca-file        $D/intermediate.crt \
          --fullchain-file $D/fullchain.crt    \
          --reloadcmd "sudo apachectl restart"

You can omit --reloadcmd "sudo apachectl restart" if you wish, however, by using it here, it gets placed in your domain's conf file for later use by --renew and --cron. I have not checked to see if --reloadcmd can be used with either of those invocations.

Manual Renewal

Some of the documentation is brief. Among other things, I was curious as to how renewals actually worked, so I performed a manual renewal against the staging endpoint.

$ acme.sh --renew --staging -d mydomain.com

This told me that the certificate was not expiring soon and it exited. Rerun the command with --renew --force, and it will renew the certificate(s), no matter their age.

What was interesting is that the above command puts the new certificates in the locations provided in the previous invocation of --install-cert. This makes sense, of course, but the documentation is silent on how all this works, so I wanted to investigate the behavior.

If you provided the --reloadcmd "apachectl restart" option to --install earlier, then the --renew command will also bounce the httpd server.

Obtain and Install "Real" Certificates

Now that I knew how things worked, I was ready to obtain and install live/valid LE certificates. To do this, I just had to rerun the previous --issue command, without the --staging parameter, followed by the same -install-cert command.

$ acme.sh --issue -d mydomain.com -w /path/to/static
$ D=/path/to/tls/certs
$ acme.sh --install-cert                       \
          -d mydomain.com                      \
          --cert-file      $D/mydomain.com.crt \
          --key-file       $D/mydomain.com.key \
          --ca-file        $D/intermediate.crt \
          --fullchain-file $D/fullchain.crt    \
          --reloadcmd "sudo apachectl restart"

I did not test it, but it is possible that a simple --renew without the --staging would have been all that was needed (perhaps with --force), i.e.:

$ acme.sh --renew -d mydomain.com
# or...
$ acme.sh --renew --force -d mydomain.com

Auto Renewal

If the --install-cert command was run with the --reloadcmd parameter, then acme.sh has already setup a cron job for you. Check by running the command crontab -l.

If you don't have a job already, one can run crontab -e and add an entry. Here's my crontab entry:

0 0 * * * "/home/ec2-user/.acme.sh/acme.sh" --cron --home "/home/ec2-user/.acme.sh" > /dev/null

This runs the --cron command once a day. Thirty days prior to their expiration, --cron will automatically obtain and install new certificates from LE. As with --renew, if you provided the --reloadcmd "apachectl restart" parameter to --install, then the --cron command will bounce the httpd server if the certificates are renewed.

You can check the acme.sh.log file to convince yourself that both the daily checks and the eventual updates are taking place as expected.

Conclusion

I found the acme.sh documentation to be a little on the light side when it comes to explaining what happens behind the scenes. This made me somewhat uneasy, but once I did a little experimentation with it, I realized that it is a fantastic alternative to the heavier weight certbot. Setup is trivial and it is rock solid. I love it. This is how I'm using LE from here on out.