'Lets Encrypt' using acme-tiny on an Unprivileged Account

As discussed here, I was pretty unhappy with letsencrypt.org's default scripts because of their need to be root and take over your machine. I spent some time with github user diafygi's letsencrypt-nosudo but found it complex and difficult. Eventually someone pointed out acme-tiny by the same guy, which is a 200 line Python script that does everything you need and can be read for security auditing purposes even by a normal human in a reasonable amount of time. The only thing I wanted to do differently from his expected use case was to do as much as possible of this as an unprivileged user (as it turns out, the author recommends this).

I created a Digital Oceans instance/droplet (Debian) for testing, and using my registrar/DNS set the name enctest.example.com to point to the new instance (okay, I've changed the name I used). I created a new unprivileged account "enc" on the machine, and did a git clone https://github.com/diafygi/acme-tiny.git in that account.

I installed nginx and added a mostly default configuration called /etc/nginx/sites-available/enctest.example.com :

server {
    listen 80;
    server_name enctest.example.com;

    root /usr/share/nginx/crypttest;
    index index.html index.htm;

    location / {
            try_files $uri $uri/ /index.html;
    }

    error_page 404 /404.html;

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
          root /usr/share/nginx/crypttest;
    }

    location ~ /\. { deny all; access_log off; log_not_found off; }

}

Changes from (Debian) defaults are server_name and two root statements. This file was then linked in /etc/nginx/sites-enabled" and nginx -s reload. Confirm that nginx is working as expected (ie. serving out of the expected local directory, not the default) for the given hostname.

We're going to generate two keys, one for communicating with letsencrypt.org and one for our domains. Both of these are one-shots: if you have other domains, these can - and should - be re-used. They don't have to be generated on the machine you're trying for the certificate on, and they certainly don't have to be generated by the particular user. I did it this way because it was easiest to follow the process outlined by acme-tiny. Keep these keys secure, re-use them, and don't store them in git repositories (or at least not ones that anyone other than yourself has access to).

As user enc: - openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -out letsencrypt.acct.private.key.pem (I like long names for clarity) - chmod 600 the generated key - this is a separate key not directly associated with your site keys: it's for talking to letsencrypt.org and apparently cannot be the same as your host key(s). Must be PEM format for acme-tiny. - repeat the process to create a private domain key: openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -out mydomains.private.key.pem (and chmod 600) - make your Certificate Signing Request: openssl req -new -sha256 -key mydomains.private.key.pem -subj "/CN=enctest.example.com" > enctest.example.com.csr - those familiar with generating CSRs will note that I'm not providing much information, only the FQDN. Not the "O" ("Organization" name, ie. "Acme Parts and Laundry") or the "OU" ("Organization Unit", ie. "Folding and Pressing Division"). I experimented with creating another CSR with more information, but (as far as I can tell) that information is ignored and not included in the resulting certificate. - mkdir ~/challenges

As root, update the nginx config:

server {
    listen 80;
    server_name enctest.example.com;

    location /.well-known/acme-challenge/ {
        alias /home/enc/challenges/;
        try_files $uri =404;
    }

    #...the rest of your config
}
  • note it's port 80!
  • note the trailing slash on the alias folder: this is essential, I dropped it on another machine and spent hours figuring out why it wasn't working
  • make sure both enc's home folder and the challenges folder are world-readable / 755 so that the web server will actually be able to serve them
  • the "challenges" folder could have been somewhere under /var/www/ with permissions to allow user "enc" to write to it, but this seemed easiest
  • I couldn't access a test file in this folder with a web browser - I got a 403 Forbidden
  • Debian's default config includes a fairly reasonable security rule:
location ~ /\. { deny all; access_log off; log_not_found off; }

But regex matching (like this one) overrides plain file matching, and this has a leading dot (like ".well-known"), so we have to override - the only change to the previous code block is to add a "^~" to the "location":

# ^~ means "if you (best) match here, don't do other regex processing!"
location ^~ /.well-known/acme-challenge/ {
    alias /home/enc/challenges/;
    try_files $uri =404;
}

This is a royal pain, but I think Debian's security line is worth keeping. Although deleting it would have solved this problem faster.

As user enc (and in the user's home directory) we run the command: python acme-tiny/acme_tiny.py --account-key ./letsencrypt.acct.private.key.pem --csr ./enctest.example.com.csr --acme-dir /home/enc/challenges/ > ./enctest.example.com.signed.crt . I'm not clear on the details, but the script generates some stuff, chats with letsencrypt.org, puts some (encrypted?) stuff in /.well-known/acme-challenge/ to prove ownership of the domain, and gets the right answer to generate the certificate.

This is the big payoff: you now have a valid certificate. It's not quite ready for use with nginx yet ... To take care of that, we run wget https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem (I don't know if this a one-time thing, or something you need to do once a year, or frequently ...). nginx needs the certificate from the signing authority combined with the generated certificate in one file: cat enctest.example.com.signed.crt lets-encrypt-x1-cross-signed.pem > enctest.example.com.chained.pem. Order is important, the site certificate has to appear before the supporting chained certificate. Unlike your domain key, this is a public document and can (and probably should, and may have to) be world readable.

Update /etc/nginx/sites-available/enctest.example.com again:

server {
    # see http://nginx.org/en/docs/http/configuring_https_servers.html for configuration tips
    listen 443 ssl;
    server_name enctest.example.com;

    ssl_certificate /home/enc/enctest.example.com.chained.pem;
    ssl_certificate_key /home/enc/mydomains.private.key.pem;
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;  # this is default, could delete this line
    ssl_session_cache shared:SSL:50m;
    ssl_prefer_server_ciphers on;

    root /usr/share/nginx/crypttest;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location ~ /\. { deny all; access_log off; log_not_found off; }

    error_page 404 /404.html;

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
          root /usr/share/nginx/crypttest;
    }
}

server {
    listen 80;
    server_name enctest.example.com;

    root /usr/share/nginx/crypttest;
    index index.html index.htm;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location ~ /\. { deny all; access_log off; log_not_found off; }

    # ^~ "if you (best) match here, don't do other regex processing!
    location ^~ /.well-known/acme-challenge/ {
        alias /home/enc/challenges/;
        try_files $uri =404;
    }

    error_page 404 /404.html;

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
          root /usr/share/nginx/crypttest;
    }
}

My plan to do this entirely as an unprivileged user falls down slightly here: nginx has to be restarted at this point to (re)load the certificate. ("enc" could be given sudo privileges for the command nginx -s reload to solve this.)

I'm not sure this process or config is totally correct, but https://enctest.example.com/ serves secure pages without a hint of a certificate problem, so it's very close to what I want. You'll notice there's a lot of duplication between the 443 and 80 sections: this can be fixed reasonably easily, see http://nginx.org/en/docs/http/configuring_https_servers.html#single_http_https_server . The /.well-known/acme-challenge/ folder has to be under 80, but nothing says it can't be served under 443 as well (even though letsencrypt.org won't look for it there).

Points for Consideration

  • making this totally non-privileged
  • why not run it as root? ... arguments can be made for either method being more secure ...
  • certs are only valid for 90 days, you're encouraged to make a cron job that renews every 60 days (I haven't done this yet). I saw someone online who renewed every 30 days, with the idea that if it failed the first time you'd still have a second attempt.
  • how does the process differ for Apache? I don't think Apache needs the chained cert, otherwise similar (although the config file styles are different)
  • why not regenerate the two private keys every time? Keep datestamped copies "in case."

Bibliography