Nadat ik laatst zo blij meldde dat ik Let's Encrypt gebruikte om DNZM een groen slotje te geven, had ik nog wel wat uitleg gepland over hoe ik dat heb gedaan. Er zijn, dat heb je zo met verse standaarden, meerdere manieren om het voor elkaar te krijgen, elk met hun voors en tegens. De "standaard" manier is via de officiële client, maar aangezien dat aan nogal wat dingen hangt en mijn eigen setup niet echt lijkt te ondersteunen (OS X + Nginx), heb ik een andere weg gekozen.

Tijd om dat eens te documenteren!

Ingrediënten

Om met mijn setup te beginnen: ik draai zoals gezegd dus Nginx onder OS X, geïnstalleerd via HomeBrew. Daar hangt verder nog PHP en zulks aan, maar dat is voor dit verhaal even niet relevant. De locaties van diverse bestanden is deels OS-gebonden (de OpenSSL-configuratie), en deels een kwestie van persoonlijke smaak.

Wat je in ieder geval nodig hebt:

  • NginX

  • Python - ik gebruik de Homebrew-versie 2.7, maar volgens mij luistert dat verder niet zo nauw

  • Acme-tiny zelf

  • Het intermediate certificaat

Hoe werkt de aanvraag

De README van acme-tiny is vrij duidelijk in wat er nodig is en wat de stappen zijn. Ik ga er even vanuit dat je een lege directory maakt en ervoor zorgt dat die niet voor iedereen en zijn moeder leesbaar is. Daarna zijn de stappen:

  1. Genereer een account private key

    Dit is een persoonlijke sleutel die je voor meerdere aanvragen kunt gebruiken; deze houd je strikt privé! Een sleutel genereer je als volgt:

    openssl genrsa 4096 > account.key
  2. Genereer een domein-sleutel en (daarmee) een certificate signing request (CSR)

    Een CSR is, zoals de naam al zegt, een verzoek om een certificaat voor je te ondertekenen. Je gebruikt ook daarvoor een sleutel, maar niet dezelfde als je account key:

    openssl genrsa 4096 > domain.key
    openssl req -new -sha256 \
    -key domain.key \
    -subj "/" \
    -reqexts SAN \
    -config <(cat $CONFIG <(printf "[SAN]\nsubjectAltName=$ALTNAMES")) \
    > domain.csr

    Dit heeft misschien een beetje uitleg nodig, althans, dat regeltje -config - voor de rest is er documentatie.

    • De variable $CONFIG die ik hier gebruik, zou normaal het pad naar je OpenSSL-configuratie-bestand moeten zijn. Op OS X is dat /System/Library/OpenSSL/openssl.cnf, op Linux is dat /etc/ssl/openssl.cnf.

    • Daarnaast is dit commando berekend op meer dan één subject name; als je een certificaat voor zowel www.example.com als example.com (dus zonder www ervoor) wil gebruiken, dan zijn dat twee aparte subject names. En het kan zijn dat je nog meer van die namen wil gebruiken; in mijn geval gebruik ik doenietzomoeilijk.nl, www.doenietzomoeilijk.nl en media.doenietzomoeilijk.nl. Bij de meeste certificaten-boeren mag je twee namen opgeven (waarbij eentje de www-versie is van de ander), en anders mag je hevig bijbetalen. Let's Encrypt laat (op dit moment) tot honderd subject names op één certificaat toe.

    Ik voeg al mijn gewenste namen samen tot één variabele $ALTNAMES. In mijn voorbeeld ziet dat er dan uit als:

    DNS:doenietzomoeilijk.nl,DNS:www.doenietzomoeilijk.nl,DNS:media.doenietzomoeilijk.nl

    Het klinkt allemaal wat bewerkelijk, maar net als je account key, hoef je je CSR maar één keer te genereren. Mocht je later een nieuwe domeinnaam willen toevoegen, dan moet je natuurlijk wel een nieuwe genereren.

  3. Zorg dat de challenge files op te roepen zijn
    Hier wijkt het proces van Let's Encrypt af van een "normale" certificatenboer: normaal gesproken zou je je CSR opsturen naar je leverancier, en wordt er een mailtje gestuurd waarin je moet bevestigen dat het domein van jou is. Let's Encrypt doet niet aan mailtjes, maar roept een bestandje op op je webserver, waarin dan een bepaalde inhoud moet staan. Elk verzoek correspondeert met één bestandje, dat daarna ook niet hergebruikt wordt. Dat is één van de dingen die acme-tiny voor je doet, gelukkig. Het enige dat je moet doen, is zorgen dat die bestandjes een plaats hebben, en dat je webserver die bestandjes kan bereiken. Het makkelijkste doe je dat door alle challenge files in één directory worden aangemaakt, en dat die ene directory voor alle relevante server-blokken in je Nginx-configuratie bereikbaar is.

    De makkelijkste oplossing was wat mij betreft een subdirectory in mijn sleutel-genereer-directory, challenges/, die in Nginx bereikbaar is via /.well-known/acme-challenge/ - die laatste naam is voorgeschreven. Let er op dat alle domeinnamen die je als subject name hebt opgegeven, ook bereikbaar moeten zijn (en afzonderlijk getest worden)!

    server {
    listen 80;
    server_name doenietzomoeilijk.nl www.doenietzomoeilijk.nl media.doenietzomoeilijk.nl;
    location /.well-known/acme-challenge/ {
    alias /pad/naar/je/challenges/;
    try_files $uri =404;
    }

    Je kunt dat location-stanza natuurlijk ook invoegen in je bestaande server-stanza's. Het heeft zin om even een testbestandje in je challenges-directory te kieperen en te kijken of dat voor alle domeinnamen te zien is!

  4. Vraag je certificaat aan

    Na al die voorbereiding is de daadwerkelijke aanvraag een peuleschil. Gelukkig maar, want juist die aanvraag zul je eens in de zoveel tijd opnieuw moeten doen.

    python acme_tiny.py \
    --account-key ./account.key \
    --csr ./domain.csr \
    --acme-dir challenges/ \
    > signed.crt

    Er wordt wat over en weer geronkt, en als alles goed is gegaan, heb je nu een kersvers certificaat in signed.crt staan! Om te zorgen dat dat certificaat overal wordt geaccepteerd, plak je er ook nog even de intermediate certificaten aan vast:

    wget -O - https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem > intermediate.pem
    cat signed.crt intermediate.pem > chained.pem

    Dat ophalen van die intermediates hoeft trouwens niet elke keer, dus als je ze eenmaal hebt kun je ze hergebruiken.

  5. Stel het certificaat in!

    Ook dat hoeft maar één keer, maar als je nieuwe certificaten hebt aangevraagd moet je Nginx wel de configuratie laten herladen om ze te gebruiken.

    Voor elke server-stanza waarvoor je https wilt aanzetten, voeg je in ieder geval de volgende regels toe:

    listen 443 ssl;
    ssl_certificate /pad/naar/chained.pem;
    ssl_certificate_key /pad/naar/domain.key;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";

    En een sudo nginx -s reload later is Bob je spreekwoordelijke oom.

Jeetje, wat een boel werk! Hoe automatiseer ik dat nou?

Toen ik eenmaal die howto had doorlopen en mijn certificaat had geïnstalleerd, was de volgende vraag natuurlijk, hoe ik dat zo makkelijk en automatisch mogelijk kon laten verlopen. Het mag ook niet te vaak fout gaan, want er zijn limieten in hoevaak je een aanvraag voor een domein kunt doen: maximaal 10 registraties per IP-adres per 3 uur, en maximaal 5 certificaten per domein per 7 dagen. Dat betekent dat als je na 5 keer nog geen certificaat hebt kunnen genereren, je een week moet wachten voor je het weer opnieuw kunt proberen.

Goed, een redelijk fool-proof scriptje dus. Ik wilde makkelijk mijn domeinen en alternatieve subject names kunnen opgeven, voorspelbare certificaat-namen hebben, en indien nodig automatisch domain keys en CSRs laten genereren. Een beetje prutsen in Bash later had ik het voor elkaar, en omdat een ander er misschien ook wat aan heeft staat het ding op GitHub. Je moet nog wel zelf voor acme-tiny en het intermediate certificate zorgen, en regelen dat Nginx bij de challenges-files kan, maar de rest neemt dit scriptje voor zijn rekening. De uiteindelijke certificaten komen netjes in een aparte directory terecht.

Wat na dat draaien van het scriptje nog wel moet, is Nginx reloaden om de nieuwe certificaten van kracht te laten worden. Dat wordt dus óf een extra cronjob, of een extra scriptje - ik wil deze versie zo algemeen mogelijk houden. Een wrapper om de wrapper... Of, en dat is misschien nog de handigste optie, ik voer dat anderhalve commando gewoon handmatig uit. Ik heb mezelf al aardig wat werk uit handen genomen.