Watch on YouTube: youtu.be/IjL3D9km3II

Updated for Let's Encrypt v2

Dec 5 Update

NOTE: Waiting for some of the kinks to be worked out before completing the article.

Goals:

  • register a domain
  • open ports to allow access to your pi
  • build letsencrypt
  • use letsencrypt to get a valid TLS (SSL) cert for HTTPS
  • start a https enabled webserver

Timeline:

  • Installation Time: 20 minutes (wait time)
  • Configuration Time: 30 seconds
  • Run Time: 30 seconds

The process is very straight-forward, but the installation takes a while (because compiling C is a slow process) and actually running the certificate registration takes longer than you'd think (because python runs very slowly on Raspberry Pi).

Step 1: Get a Let's Encrypt client

I would strongly recommend caddy, but at the time of this writing (Dec 5th) it seems to not work yet. Expect an update this week.

That leaves us with the python client at https://letsencrypt.readthedocs.org/en/latest/using.html#getting-the-code

Step 2: Install the Let's Encrypt python client

git clone https://github.com/letsencrypt/letsencrypt
pushd letsencrypt

./bootstrap/debian.sh

./letsencrypt-auto

Step 3: Get a Certificate

sudo ~/.local/share/letsencrypt/bin/letsencrypt certonly \
  --agree-tos \
  --email john.doe@example.com \
  --domains example.com,www.example.com \
  --standalone

For more options see the docs

sudo ~/.local/share/letsencrypt/bin/letsencrypt --help all

Step 4: Test your certs

TODO: I would love to tell you where the certificates go, but I'm currently blocked by issue #1228

Troubleshooting

Firewall

Both ports 80 and 443 are used by letsencrypt.

If you have a firewall (such as ufw), make sure that you allow those ports through:

sudo ufw allow http
sudo ufw allow https

Getting Certs without Restarting

You run your webserver on ports 80 and 443, right?

Yet you also have to respond to cert challenges and receive your certs on 80 and 443, right?

This presents a problem, but luckily it can be solved with HAProxy:

Port Forwarding

If you rely on port forwarding, (as is common for a Raspberry Pi on your home network) make sure you allow those ports through.

Original Post

You may also be interested in

Goals:

  • register a domain
  • open ports to allow access to your pi
  • build letsencrypt
  • use letsencrypt to get a valid TLS (SSL) cert for HTTPS
  • start a https enabled webserver

Following the instructions at https://letsencrypt.readthedocs.org/en/latest/using.html#getting-the-code

The Easy Way

Soon caddy will have built-in letsencrypt support and it will also have an API that allows updating certificates and routes on-the-fly.

Install and Build letsencrypt

This process is super slow on a raspberry pi because it has to compile a metric ton of dependencies for python.

I think it was in the 5 to 10-minute range.

git clone https://github.com/letsencrypt/letsencrypt
pushd letsencrypt

sudo ./bootstrap/debian.sh

virtualenv --no-site-packages -p python2 venv
./venv/bin/pip install -r requirements.txt acme/ .

Grab some munchies and wait it out...

On the Raspberry Pi model B it can take quite a while.

1078.53user 56.81system 21:13.52elapsed 89%CPU (0avgtext+0avgdata 85260maxresident)k
356600inputs+491984outputs (485major+669516minor)pagefaults 0swaps

On the B+ and 2.0 there is much more memory, which is the bulk of the holdup.

And if you have a firewall, don't forget to allow port 443/tcp (and probably 80/tcp too)

sudo ufw allow http
sudo ufw allow https

Get a certificate for your Domain

sudo mkdir -p /etc/letsencrypt/
sudo vim /etc/letsencrypt/cli.ini

/etc/letsencrypt/cli.ini:

# This is an example configuration file for developers
#config-dir = /tmp/le/conf
#work-dir = /tmp/le/conf
#logs-dir = /tmp/le/logs

# make sure to use a valid email and domains!
email = foo@example.com
#domains = example.com,www.example.com

text = True
agree-eula = True
debug = True
# Unfortunately, it's not possible to specify "verbose" multiple times
# (correspondingly to -vvvvvv)
verbose = True

authenticator = standalone

This is also ridiculously slow on the Raspberry Pi (a minute or two).

The forthcoming node and go clients should be significantly faster (perhaps in the range of 10 to 20 seconds).

Standalone mode

sudo ./venv/bin/letsencrypt \
  --email coolaj86@gmail.com \
  --domains dangerpi.devices.coolaj86.com \
  --authenticator standalone \
  auth

For reference, on the Raspberry Pi model A this took about a minute

96.99user 1.01system 1:52.08elapsed 87%CPU (0avgtext+0avgdata 29296maxresident)k
2176inputs+160outputs (0major+17006minor)pagefaults 0swaps

Manual mode

sudo ./venv/bin/letsencrypt \
  --email coolaj86@gmail.com \
  --domains dangerpi.devices.coolaj86.com \
  --authenticator manual \
  --dvsni-port 443 \
  auth

Other userful options

  • --agree-eula
  • --text
  • --authenticator manual
  • --authenticator standalone

You will need to specify manual or automatic mode.

Automatic vs Manual

I've had success with both manual mode and standalone mode.

You'll have to open 443 and run as root. There's no way to specify another port as per the spec, though there are reasonable arguments to change the spec.

Minimizing Downtime for a Production WebServer

If you already have a webserver running on 443, you might want to configure happroxy to front for that webserver and have a secondary webserver that handles SNI passes requests to *.acme.invalid to letsencrypt running on an alternate port using the --dvsni-port option. That server needs to be able to statically serve traffic to /.well-known/acme-challenge/.

Manual-Mode One-Off Server

When you choose manual mode you will be given the instruction to run commands similar to these:

mkdir -p .well-known/acme-challenge

# write the challenge out to a file
echo -n K0haDCp1jSTnrhTFSrdQML5SjgJp1prYSoXM6Jdab5c \
  > .well-known/acme-challenge/gQrWUQIHZc_dP5NsvirVlsRa

# create a self-signed root ca to serve as if it were a certificate
openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 \
  -keyout key.pem \
  -out cert.pem

# create a one-off https server to issue the certificate
sudo python -c "import BaseHTTPServer, SimpleHTTPServer, ssl; \
s = BaseHTTPServer.HTTPServer(('', 443), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.socket = ssl.wrap_socket(s.socket, keyfile='key.pem', certfile='cert.pem'); \
s.serve_forever()"

You will need to do this in a new terminal window. As soon as the certificates are granted you can stop this server.

Result

You will likely have some warning messages about insecure certificates (the one-off server) and also some success messages.

If all went you can run tree /etc/letsencrypt/ and you'll see your domain in a structure like this:

Install and run tree

sudo apt-get install --yes tree
tree /etc/letsencrypt/

The output should look like this:

/etc/letsencrypt/
├── accounts
│   └── www.letsencrypt-demo.org
│       └── acme
│           └── new-reg
│               ├── coolaj86@gmail.com
│               └── keys
│                   └── 0000_coolaj86@gmail.com
├── archive
│   └── dangerpi.devices.coolaj86.com
│       ├── cert1.pem
│       ├── chain1.pem
│       ├── fullchain1.pem
│       └── privkey1.pem
├── certs
│   └── 0000_csr-letsencrypt.pem
├── configs
│   └── dangerpi.devices.coolaj86.com.conf
├── keys
│   └── 0000_key-letsencrypt.pem
└── live
    └── dangerpi.devices.coolaj86.com
        ├── cert.pem -> ../../archive/dangerpi.devices.coolaj86.com/cert1.pem
        ├── chain.pem -> ../../archive/dangerpi.devices.coolaj86.com/chain1.pem
        ├── fullchain.pem -> ../../archive/dangerpi.devices.coolaj86.com/fullchain1.pem
        └── privkey.pem -> ../../archive/dangerpi.devices.coolaj86.com/privkey1.pem

The important files are the key, cert, and chain, you can read about them here:

/etc/letsencrypt/live/dangerpi.devices.coolaj86.com/
├── cert.pem        # the server certificate only
├── chain.pem       # the intermediate ca(s)
├── fullchain.pem   # cert.pem + chain.pem
└── privkey.pem     # the server's private key

Note: until the real certificates are issued in September 2015, the "happy hacker" Root CA is being issued in place of the intermediate certificate.

Note: generally speaking the order of concatonated certificates should be most-specific (least authoritative) to least-specific (most authoritative) - such as [cert, intermediate, root] and not [root, intermediate, cert]. This improves compatibility with some servers.

Note: I've seen some servers that like cat cert.pem privkey.pem > server.pem and some that do not like the Root CA to be in the chain or fullchain.

Testing your Certs

You can use serve-https to quickly test your certificates straight from the /etc/letsencrypt.

npm install -g serve-https

sudo serve-https -p 8443 --letsencrypt-certs foo.example.com --serve-chain true

curl --insecure https://foo.example.com:8443 > chain.pem
curl https://foo.example.com:8443 --cacert chain.pem

See serve-https.

Using your Certs with Node.js

In node.js you would use these like so:

'use strict';

var fs = require('fs');
var path = require('path');
var letsetc = '/etc/letsencrypt/live/';
var defaultdomain = 'dangerpi.devices.coolaj86.com';

function getSecureContext(domainname, opts) {
  if (!opts) { opts = {}; }

  opts.key = fs.readFileSync(path.join(letsetc, domainname, 'privkey.pem'))
  opts.cert = fs.readFileSync(path.join(letsetc, domainname, 'cert.pem'));
  opts.ca = fs.readFileSync(path.join(letsetc, domainname, 'chain.pem'), 'ascii')
    .split('-----END CERTIFICATE-----')
    .filter(function (ca) {
      return ca.trim();
    }).map(function (ca) {
      return (ca + '-----END CERTIFICATE-----').trim();
    });

  return require('tls').createSecureContext(opts);
}

//
// SSL Certificates
//
var options = {
, requestCert: false
, rejectUnauthorized: true

  // If you need to use SNICallback you should be using io.js >= 1.x (possibly node >= 0.12)
, SNICallback: function (domainname, cb) {
    var secureContext = getSecureContext(domainname);
    cb(null, secureContext);
  }
  // If you need to support SPDY/HTTP2 this is what you need to work with
//, NPNProtocols: ['http/2.0', 'spdy', 'http/1.1', 'http/1.0']
  , NPNProtocols: ['http/1.1']
};

// Start the server
server = https.createServer(getOpts(defaultdomain, options));
server.on('error', function (err) {
  console.error(err);
});
server.listen(443, function () {
  console.log('Listening');
});
server.on('request', function (req, res) {
  res.end('hello');
});

By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )