Let's Encrypt (updated)

Update: since this was written, the letsencrypt-auto script has improved significantly. When I tried it again today (December 8, 2015), the process was basically just cloning the GitHub repo and running ./letsencrypt-auto. I’ll leave the original (outdated) information here for posterity.

As of today phiffer.org is being served using SSL encryption thanks to a free certificate from Let’s Encrypt. It’s a recently launched service, sponsored by Mozilla and the Electronic Frontier Foundation (among others), intended to make HTTPS encryption ubiquitous on the web.

Hooray for Let's Encrypt!
Hooray for [Let's Encrypt!](https://letsencrypt.org/)

Let’s Encrypt is very new, and there are still some rough edges, but overall I’m impressed by how smoothly the process went. I wanted to document my experience, in case it’s helpful to others (and future-me). This post is a bit more technical than usual and, because the service is new, much of it may not be relevant very long into the future. That said, I hope this might offer some clues for folks trying to get up and running on HTTPS.

To begin the process, sign up for the private beta program (announced here). Yes, it’s just a Google Doc.

Next you will need to download a command-line tool (documentation) onto the server where you’ll be installing the certificates. (I think you can also run the tool from a different computer and just upload the certificate files at the end.)

git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
./letsencrypt-auto

The command-line utility launches as an ncurses-style interface, asking you to agree to certain things and enter some information. I advanced a few steps and encountered my first error message: “ASN1_mbstring_ncopy string too short.” I searched for clues about what might be happening, but found myself at an impasse.

Just as I was about to call it a day, I got an email from Let’s Encrypt Beta. I was in! The timing of the email made me wonder if somehow my beta invitation was activated through my using the command line tool. Who knows, it could just be a coincidence.

The email included a new command to try out. Their version had some new arguments added into the mix.

./letsencrypt-auto --agree-dev-preview --server \
  https://acme-v01.api.letsencrypt.org/directory auth

The interface offered a choice: would I like to install for Apache or a free-standing web server. The Apache support seems to be specifically focused on Debian-style installs (with /etc/apache2/sites-enabled virtual hosts), and I found it not quite robust enough to account for my configuration edge-cases.

For example, I use mod_macro to avoid duplicate configurations, and the script wasn’t able to detect my macro virtual host. I converted it to something more ordinary, but still kept getting the error: “Only one vhost per file is allowed”.

After looking over the documentation I decided to try the manual configuration option rather than let the utility configure Apache for me. This ended up working out well for me.

./letsencrypt-auto -a manual \
  --agree-dev-preview --server \
  https://acme-v01.api.letsencrypt.org/directory auth

During the manual process I was prompted to copy/paste commands and then press enter to continue. I didn’t follow the instructions precisely, but they included enough clues to figure out how to proceed.

At one point I was asked to set up a file at a specified URL to prove ownership over my domain. letsencrypt-auto suggested that I stand up a new Python-based web server that could serve traffic on phiffer.org. But that would mean shutting down my live websites, served on Apache, while I confirmed ownership. Instead I just dropped the files I needed onto my public web directory. The commands I used looked like:

cd phiffer.org
mkdir -p .well-known/acme-challenge
echo -n '{"header": {"alg": "RS256", [...] }' > vEBssphV2He1gNjcJ--3AZt0TVtxGTOs3d8A5o85Tak

The challenge file was supposed to be served as Content-Type application/jose+json so I set up an .htaccess file in the .well-known/acme-challenge folder with the following configuration:

RewriteRule .* - [T=application/jose+json]

I double checked that it was working by loading up the URL in a browser, and continued through the process. At the end I got a message that it worked; my certificate had been saved at /etc/letsencrypt/live/phiffer.org/fullchain.pem.

After a short celebration, I studied the default Apache SSL configuration and created a basic virtual host for HTTPS:

<IfModule mod_ssl.c>
  <VirtualHost _default_:443>
    DocumentRoot /var/www/phiffer.org

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/phiffer.org/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/phiffer.org/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/phiffer.org/chain.pem

    <FilesMatch "\.(cgi|shtml|phtml|php)$">
      SSLOptions +StdEnvVars
    </FilesMatch>
    <Directory /usr/lib/cgi-bin>
      SSLOptions +StdEnvVars
    </Directory>

    BrowserMatch "MSIE [2-6]" \
      nokeepalive ssl-unclean-shutdown \
      downgrade-1.0 force-response-1.0
    # MSIE 7 and newer should be able to use keepalive
    BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
  </VirtualHost>
</ifModule>

I restarted Apache (several times, after getting lots of things wrong), and I was up and running with HTTPS! There were a few other tweaks I made:

Finally, once I was happy with how the HTTPS version of the site was working, I set up a redirect to switch all of my plain vanilla HTTP traffic over to HTTPS. For reference, here is my full phiffer.org.conf file:

<VirtualHost *:80>
  ServerName phiffer.org
  ServerAlias www.phiffer.org new.phiffer.org old.phiffer.org
  DocumentRoot /var/www/phiffer.org
  ErrorLog ${APACHE_LOG_DIR}/phiffer.org/error.log
  CustomLog ${APACHE_LOG_DIR}/phiffer.org/access.log combined
  PHP_Value error_log ${APACHE_LOG_DIR}/phiffer.org/php.log

  RewriteEngine On
  RewriteRule ^/(.*)$ https://phiffer.org/$1 [R,L]
</VirtualHost>

<IfModule mod_ssl.c>
  <VirtualHost _default_:443>
    ServerAdmin dan@phiffer.org
    DocumentRoot /var/www/phiffer.org
    ErrorLog ${APACHE_LOG_DIR}/phiffer.org/error.log
    CustomLog ${APACHE_LOG_DIR}/phiffer.org/access.log combined
    PHP_Value error_log ${APACHE_LOG_DIR}/phiffer.org/php.log

    RewriteEngine On
    RewriteCond %{QUERY_STRING} transport=polling
    RewriteRule /(.*)$ http://localhost:3000/$1 [P]
    ProxyRequests off
    ProxyPass /socket.io/ wss://localhost:3000/socket.io/
    ProxyPassReverse /socket.io/ wss://localhost:3000/socket.io/

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/phiffer.org/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/phiffer.org/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/phiffer.org/chain.pem

    <FilesMatch "\.(cgi|shtml|phtml|php)$">
      SSLOptions +StdEnvVars
    </FilesMatch>
    <Directory /usr/lib/cgi-bin>
      SSLOptions +StdEnvVars
    </Directory>

    BrowserMatch "MSIE [2-6]" \
      nokeepalive ssl-unclean-shutdown \
      downgrade-1.0 force-response-1.0
    # MSIE 7 and newer should be able to use keepalive
    BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown

    # Harden SSL ciphers
    SSLProtocol ALL -SSLv2 -SSLv3
    SSLHonorCipherOrder On
    SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
    SSLCompression Off
  </VirtualHost>
</IfModule>

And here is that WordPress filter, in case you have a similar need:

function protocol_agnostic_urls($content) {
  $home_url = 'http://phiffer.org';
  $fixed_url = str_replace('http:', '', $home_url);
  $content = str_replace($home_url, $fixed_url, $content);
  return $content;
}
add_filter('the_content', 'protocol_agnostic_urls', 99);