Chris Hager
Programming, Technology & More

Comparison of 10 ACME / Let's Encrypt Clients

Let's Encrypt Logo

Let’s Encrypt is a new certificate authority backed by Mozilla, Akamai, EFF, Facebook and others, which provides free, automated SSL/TLS certificates. The public beta started on December 3, 2015 and a whole lot of certificates have been issued already:

Let's Encrypt Daily Activity

Several clients to automate issuing, renewing and revoking certificates have been released both by the community and the Let’s Encrypt team. This post is an overview and comparison of 10 popular Let’s Encrypt clients:



The Automated Certificate Management Environment (ACME) protocol defines a way of automatically obtaining trusted certificates without human intervention. First, the control of a domain has to be proven, then the agent can request, renew and revoke certificates:

ACME Challenge

Certificates issued by Let’s Encrypt are valid for 90 days, and are expected to be renewed automatically. More background information can be found on the Let’s Encrypt - How It Works page.

At the time of writing, these rate limits has been in place:

  • 10 Registrations per IP per 3 hours
  • 5 Certificates per Domain per 7 days (incl. subdomains)

SSL Certificates & Signing

Obtaining a valid SSL certificate generally includes the following steps:

  1. You create a private and public key pair on the server.
  2. You create a Certificate Signing Request (CSR) which includes the domain name, organization name, the public key, and other information. The CSR is signed with your private key.
  3. You send the CSR to the certificate authority (in this case Let’s Encrypt).
  4. The certificate authority signs the request, thus producing a public certificate.
  5. You use the public certificate in your webserver.

For more information on configuring a webserver with certificates, check out these links:

Client Analysis

These tests are going to obtain a certificate for a domain such as and setting up automatic certificate renewal.

Domain ownership verification requires the ACME server being able to access a specific file on the domain. To accomplish this, we assume a webserver is running and serves files from /var/www/htdocs/ (the webroot) and it’s subdirectories. For instance, a file at /var/www/htdocs/.well-known/acme-challenge/8303 should be accessible via

Official Let’s Encrypt Client

The official Let’s Encrypt client, letsencrypt-auto, is a heavyweight, fully automated Python program with various modes of operation and installers (for instance to automagically update Apache and nginx configurations).

The standard client automatically installs various system dependencies via the standard package manager (see the source code and /bootstrap/), and sets up a virtualenv with a number of Python dependencies. The client is also available as a Docker image, which avoids the necessity to install packages system-wide.

In case of an error (eg. the ACME server is not reachable), the official client gracefully terminates with an info message.

The official Let’s Encrypt client also supports config files, which may be easier to automate with a cronjob (see this post for more infos).

  •   Official client, active community
  •   Well documented, and well tested
  •   Can do a lot of things, including server configs (experimental!)
  •   Accessible for non-technical users
  •   Complex program, many moving parts, hard to review
  •   Runs as root and installs dependencies without asking

Standard Installation

$ git clone
$ cd letsencrypt
$ ./letsencrypt-auto --help

⇾ show output

Obtaining a certificate

To obtain a certificate without stopping a running webserver, we are going to use the webroot method, which only requires write access for the webroot to save the authentication file.

$ sudo ./letsencrypt-auto certonly \
    --webroot --webroot-path /var/www/htdocs/ \
    --email \

This command creates the private and public keys, generate a certificate signing request, get the challenge from the ACME server, saves it to the webroot, and downloads the signed certificate in /etc/letsencrypt/live/ The process is fully automated and results in the certificate and private key, ready to be used by your webserver.

⇾ show list of generated files

Alternatively the offial Let’s Encrypt client includes a manual plugin, which can generate a certificate from another computer than the webserver (akin to or letsencrypt-nosudo). You can run this plugin with the command ./letsencrypt-auto certonly --manual.


To renew certificates automatically, simply add the --renew parameter to the above command:

$ sudo ./letsencrypt-auto certonly \
    --renew \
    --webroot --webroot-path /var/www/htdocs/ \
    --email \


Acme-tiny is a tiny Python script which assists with issuing and renewing certificates. You generate the private key and create a certificate signing request (CSR) manually, and acme-tiny handles the rest (submitting the CSR to the ACME server, receiving the authentication files, putting it in the acme-challenge folder and receiving the final certificate). The output of this script is the signed certificate.

  •   Documentation (in the README)
  •   Super simple, no dependencies
  •   Easy to embed in custom Python application


# Get a copy of acme-tiny
$ cd /opt
$ git clone
$ cd acme-tiny
$ python --help

⇾ show output

Obtaining a certificate

# Create a directory for the keys and cert
mkdir -p /etc/letsencrypt/
cd /etc/letsencrypt/

# Generate a private key
openssl genrsa 4096 > account.key

# Generate a domain private key (if you haven't already)
openssl genrsa 4096 > domain.key

# Create a CSR for
openssl req -new -sha256 -key domain.key -subj "/" > domain.csr

# Create the challenge folder in the webroot
mkdir -p /var/www/htdocs/.well-known/acme-challenge/

# Get a signed certificate with acme-tiny
python /opt/acme-tiny/ --account-key ./account.key --csr ./domain.csr --acme-dir /var/www/htdocs/.well-known/acme-challenge/ > ./signed.crt

# Append the Let's Encrypt intermediate cert to your cert
wget -O - > intermediate.pem
cat signed.crt intermediate.pem > chained.pem

⇾ show output

At this point chained.pem contains the signed certificate chain and, along with domain.key, can be used to run a http server (more infos).

Renewal simply requires running this script again with the same parameters. is a website which helps users to manually generate all the necessary information to create a certificate signing request (CSR), guiding a user through the whole ACME process:

  1. Manually create a public and private key.
  2. Manually create a certificate signing request (CSR).
  3. Manually sign a number of requests with the private key.
  4. Manually verify ownership by serving the signed requests (either through an existing webserver or a simple Python webserver).
  5. The website tells the ACME server to check for the verifications, and in case of success provides you with the signed certificate.

The website provides OpenSSL commands at each step, and waits for the output of the commands to be pasted back into the website to verify the success. The website never asks for any kind of private key. It can be saved and used locally without any server side logic.

  •   Works as advertised
  •   Static site allows to save and use later


    • Github Stars Github Forks
    • 112 commits, 9 contributors
  • Mode: Fully or Semi-Automatic
  • License: GPLv3
  • Language: Python
  • Lines of code: 775 Python, 126 Shell Script, 41 YAML
  • Dependencies: Some

simp_le is an ACME client written in Python. It works similar to acme-tiny, but can also generate the private key and CSR automatically. Requires a small number of dependencies to be installed.


$ cd /opt
$ git clone
$ cd simp_le

# Setup dependencies and run
$ ./
$ ./
$ ./venv/bin/simp_le --help

⇾ show output

Obtaining a certificate

$ ./venv/bin/simp_le \
    -f account_key.json -f key.pem -f cert.pem -f fullchain.pem \
    --email \

On success, this command produces 4 files: account_key.json, cert.pem, fullchain.pem and key.pem, which can be used from your webserver’s SSL configuration (see here for an example integration in Apache).

Renewal works by using the same command.

Exit codes:

  • 0 if certificate data was created or updated;
  • 1 if renewal not necessary;
  • 2 in case of errors.


This program is a predecessor of acme-tiny and functionally equivalent to (made by the same author). The script guides you through the whole process and ask you do run all the necessary commands in the terminal.

You generate a private key and certificate signing request (CSR), then run to get the signed certificate. The script goes through the ACME protocol with the Let’s Encrypt certificate authority and outputs the signed certificate to stdout.


$ cd /opt
$ git clone
$ cd letsencrypt-nosudo
$ python -h

⇾ show output

Obtaining a certificate

$ openssl genrsa 4096 > user.key
$ openssl rsa -in user.key -pubout >
$ openssl genrsa 4096 > domain.key
$ openssl req -new -sha256 -key domain.key -subj "/" > domain.csr
$ python --public-key domain.csr > signed.crt

⇾ show output


    • Github Stars Github Forks
    • 103 commits, 1 contributors
  • Mode: Automatic / Interactive
  • License: MIT
  • Language: Go
  • Lines of code: ~6,000
  • Dependencies: None (Binary Release)

acmetool is an ACME client written in Go, supporting automatic domain verification with webroot and standalone methods as well as an interactive wizard. acmetool stores credentials and certificates at /var/lib/acme/live/HOSTNAME/{cert,chain,fullchain,privkey} by default and includes support to import certificates from the official client.

acmetool furthermore provides a reconcile option which makes sure all desired hostnames have valid certificates which don’t expire soon.


You can either get a binary release, or build from source as described in the Readme:

$ wget
$ tar -xvf acmetool-v0.0.22-linux_amd64.tar.gz
$ cd acmetool-v0.0.22-linux_amd64
$ bin/acmetool --help

⇾ show output

Obtaining a certificate

You need to set the webroot to /var/run/acme/acme-challenge as described in the docs, or use the proxy method. The command acmetool want tries all available methods.

$ sudo acmetool want


    • Github Stars Github Forks
    • 162 commits, 4 contributors
  • Mode: Automatic
  • License: MIT
  • Language: Go
  • Lines of code: ~2,000
  • Dependencies: None (Binary Release)
  • Show HN

Lego is an ACME library and standalone application written in Go. It can be downloaded as a binary release or build by yourself.

Running the standalone version requires the permission to bind to port 80 and 443, which conflicts with a webserver which is already running.


$ wget
$ tar -xvf lego_linux_amd64.tar.gz
$ ./lego help

⇾ show output

Obtaining a certificate

$ sudo ./lego --email="" --domains="" run

⇾ show output


$ git clone
$ ./ --help

⇾ show output

Obtaining a certificate

Create a file called domains.txt which contains the domains and subdomains you want to generate certificates for:

This requests two certificates, for and The other domains in the corresponding line are their alternative names. writes the challenge files by default into the directory "${SCRIPTDIR}/.acme-challenges". To adjust this to your webroot, you need to create a config file ( with another $WELLKNOWN path (see


Then simply run (⇾ show output).


acme-client is an ACME client written in PHP, built on top of the acme PHP library by the same author.

It requires PHP 7 and composer to install it’s dependencies.


$ git clone
$ cd acme-client
$ composer install

Obtaining a certificate

# Register the Let's Encrypt account
$ sudo bin/acme register \
    --server \

# Issue the certificate
$ sudo bin/acme issue \
    --server \
    --domains, \
    --path /var/www/


    • Github Stars Github Forks
    • 3 commits, 1 contributors
  • Mode: Automatic
  • License: BSD
  • Language: PHP
  • Lines of code: ~450
  • Dependencies: PHP 5.4.8+ with OpenSSL and curl extension
  • Show HN

lescript is a very simplistic PHP ACME client library, with an example CLI wrapper.


$ git clone
$ cd lescript

Obtaining a certificate

Use the library as shown in _example.php.


ClientUser ModeDeps.LanguageLOCLicenseCapabilitiesDomain Authentication
Official Let's Encrypt Client (Docs)Automatic / Interactive / ManualManyPython~8.600Apache 2.0Issue, Renew, Revoke, Server ConfigWebroot, Standalone, Manual
acme-tinyAutomatic / Semi-AutomaticNonePython~200MITIssue, RenewWebroot
Semi-ManualNoneHTML/JS~1200MITIssue, RenewWebroot
simp_leAutomatic / Semi-AutomaticSomePython~800GPLv3Issue, Renew, RevokeWebroot
letsencrypt-nosudoManual (assisted)NonePython~400AGPLIssue, Renew, RevokeStandalone
acmetoolAutomatic / InteractiveNoneGo~6,000MITIssue, RenewWebroot, Standalone
legoAutomaticNoneGo~2,000MITIssue, Renew, RevokeStandalone
letsencrypt.shAutomaticNoneBash~600?Issue, Renew, RevokeWebroot
acme-clientSemi-AutomaticSome (incl. PHP 7)PHP~400MITIssue, Renew, RevokeWebroot
lescriptSemi-AutomaticNonePHP~450BSDIssue, RenewWebroot

If you have suggestions or feedback, please let me know via @metachris.

Creating standalone Mac OS X applications with Python and py2app

In this tutorial we’ll be using py2app to create a standalone OSX application from a Python 2 or 3 source code with a simple Tkinter user interface.

"py2app is a Python setuptools command which will allow you to make standalone application bundles and plugins from Python scripts. py2app is similar in purpose and design to py2exe for Windows."

Relevant links about py2app:

This guide is loosely based on the official tutorial. Based on a Python file called, we’ll create an application called

App Icon


Create a custom directory and create a virtualenv:

# Create a custom directory
$ mkdir SandwichApp
$ cd SandwichApp

# Use virtualenv to create an isolated environment
$ virtualenv venv
$ . venv/bin/activate

Now create a very simple Tkinter app with the filename

import sys
if sys.version_info < (3, 0):
    # Python 2
    import Tkinter as tk
    # Python 3
    import tkinter as tk
root = tk.Tk()
tk.Button(root, text="Make me a Sandwich").pack()

This little app will look like this:

App Screen

Install py2app

The original version of py2app has a bug due to a newer version of ModuleGraph. I made a fork of the project and fixed this bug on Github. Install it with pip like this:

$ pip install -U git+

Create a file

py2app includes py2applet, a helper which generates a file for you:

$ py2applet --make-setup

This is a basic definition of the app:

from setuptools import setup

APP = ['']
OPTIONS = {'argv_emulation': True}

    options={'py2app': OPTIONS},

If your application uses some data files, like a JSON, text files or images, you should include them in DATA_FILES. For example:

DATA_FILES = ['testdata.json', 'picture.png']

Build the app for development and testing

py2app builds the standalone application based on the definition in

For testing and development, py2app provides an “alias mode”, which builds an app with symbolic links to the development files:

$ python py2app -A

This creates the following files and directories:

├── build
│   └── bdist.macosx-10.10-x86_64
│       └── python2.7-standalone
│           └── app
│               ├── Frameworks
│               ├── collect
│               ├── lib-dynload
│               └── temp
├── dist
│   └──
│       └── Contents
│           ├── Info.plist
│           ├── MacOS
│           │   ├── Sandwich
│           │   └── python -> /Users/chris/Projects/chris/python-gui/tkinter/env/bin/../bin/python
│           ├── PkgInfo
│           └── Resources
│               ├──
│               ├──
│               ├── lib
│               │   └── python2.7
│               │       ├── config -> /Users/chris/Projects/chris/python-gui/tkinter/env/bin/../lib/python2.7/config
│               │       └── site.pyc -> ../../site.pyc
│               ├──
│               └── site.pyc

This is not a standalone application, and the applications built in alias mode are not portable to other machines!

The app built with alias mode simply references the original code files, so any changes you make to the original file are instantly available on the next app start.

The resulting development app in dist/ can be opened just like any other .app with the Finder or the open command ($ open dist/ To run your application directly from the Terminal you can just run:

$ ./dist/

Building for deployment

When everything is tested you can produce a build for deployment with a calling python py2app. Make sure that any old build and dist directories are removed:

$ rm -rf build dist
$ python py2app

This will assemble your application as dist/ Since this application is self-contained, you will have to run the py2app command again any time you change any source code, data files, options, etc.

The original py2app has a bug which would display “AttributeError: 'ModuleGraph' object has no attribute 'scan_code'” or load_module. If you encounter this error, take a look at this StackOverflow thread or use my fork of py2app.

The easiest way to wrap your application up for distribution at this point is simply to right-click the application from Finder and choose “Create Archive”.

Adding an icon

Simply add "iconfile": "youricon.icns" to the OPTIONS dict:

from setuptools import setup

APP = ['']
    'argv_emulation': True,
    'iconfile': 'app.icns'

    options={'py2app': OPTIONS},

You can find free icons in icns format around the web (eg. on IconFinder or freepik).

Advanced app settings

You can tweak the application information and behaviour with modifications to the Info.plist. The most complete reference for the keys available is Apple’s Runtime Configuration Guidelines.

Here is an example with more modifications:

# -*- coding: utf-8 -*-
from setuptools import setup

APP = ['']
APP_NAME = "SuperSandwich"

    'argv_emulation': True,
    'iconfile': 'app.icns',
    'plist': {
        'CFBundleName': APP_NAME,
        'CFBundleDisplayName': APP_NAME,
        'CFBundleGetInfoString': "Making Sandwiches",
        'CFBundleIdentifier': "com.metachris.osx.sandwich",
        'CFBundleVersion': "0.1.0",
        'CFBundleShortVersionString': "0.1.0",
        'NSHumanReadableCopyright': u"Copyright © 2015, Chris Hager, All Rights Reserved"

    options={'py2app': OPTIONS},

With these settings, the app will have the following infos:

Finder Get Info


See Also

If you have suggestions, feedback or ideas, please reach out to me @metachris.

Python Helpers for String/Unicode Encoding, Decoding and Printing

String encoding and decoding as well as encoding detection can be a headache, more so in Python 2 than in Python 3. Here are two little helpers which are used in PDFx, the PDF metadata and reference extractor:

  • make_compat_str - decode any kind of bytes/str into an unicode object
  • print_to_console - print (unicode) strings to any kind of console (even windows with cp437, etc.)

All of this code is in the public domain via The Unlicense.


print_to_console detects the output locale and tries to correctly encode the given (unicode) string. Using this you can safely print to any kind of terminal, either support UTF-8 or any other encoding (eg. Windows with cp437). Fallback to ascii with backslash-replace:

def print_to_console(text):
    # Prints a (unicode) string to the console, encoded depending on the stdout
    # encoding (eg. cp437 on Windows). Works with Python 2 and 3.
    except UnicodeEncodeError:
        bytes_string = text.encode(sys.stdout.encoding, 'backslashreplace')
        if hasattr(sys.stdout, 'buffer'):
            text = bytes_string.decode(sys.stdout.encoding, 'strict')


make_compat_str detects the encoding of a string or bytes object using chardet, and returns a standard unicode object. Just throw any kind of bytes / string at it!

import sys
import chardet

IS_PY2 = sys.version_info < (3, 0)
if not IS_PY2:
    # Helper for Python 2 and 3 compatibility
    unicode = str

def make_compat_str(in_str):
    Tries to guess encoding of [str/bytes] and decode it into
    an unicode object.
    assert isinstance(in_str, (bytes, str, unicode))
    if not in_str:
        return unicode()

    # Chardet in Py2 works on str + bytes objects
    if IS_PY2 and isinstance(in_str, unicode):
        return in_str

    # Chardet in Py3 works on bytes objects
    if not IS_PY2 and not isinstance(in_str, bytes):
        return in_str

    # Detect the encoding now
    enc = chardet.detect(in_str)

    # Decode the object into a unicode object
    out_str = in_str.decode(enc['encoding'])

    # Cleanup: Sometimes UTF-16 strings include the BOM
    if enc['encoding'] == "UTF-16BE":
        # Remove byte order marks (BOM)
        if out_str.startswith('\ufeff'):
            out_str = out_str[1:]

    # Return the decoded string
    return out_str

If you have suggestions, feedback or ideas, please reach out to @metachris.

Blog Archive