Mémo sur la génération d’un certificat TLS avec let’s encrypt

Ayant participé à la bêta privée de let’s encrypt, j’ai pu jouer un peu avec, en particulier pour automatiser la génération de certificat, puisque quand j’ai commencé, il n’y avait pas d’intégration avec nginx et maintenant je ne suis pas friand de laisser un programme modifier mes vhosts.

Installation

Commencez par récupérer l’outils :

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

Je vous conseille de lancer l’interface afin de lire les conditions d’utilisation du service et de vous familiariser avec son fonctionnement (informations demandées et challenge de validation).

Bon l’interface est sympa, mais la lancer tous les trois mois pour générer des dizaines de certificats1, ce n’est pas envisageable. Passons donc à l’automatisation.

Automatisation

Créez un fichier /etc/letsencrypt/config.cli.ini :

text = True
agree-dev-preview = True
rsa-key-size = 4096
agree-tos = True
authenticator = webroot
webroot-path = /tmp/letsencrypt/public_html

Le plus important ici, est les deux dernières options qui permettent de gérer l’authentification avec notre propre serveur http. Let’s encrypt va déposer les fichiers dans webroot-path et tenter de les récupérer via le nom de domaine du certificat dans le sous-dossier .well-known.

Voici donc la configuration pour nginx à placer dans chacun de vos vhost :

location /.well-known/acme-challenge {
    add_header Content-Type application/jose+json;
    root /tmp/letsencrypt/public_html;
}

Pendant que vous êtes dans la configuration de nginx, profitez en pour générer une configuration TLS convenable  : https://mozilla.github.io/server-side-tls/ssl-config-generator/.

Pour finir le script à placer en crontab pour mettre à jour l’ensemble de vos certificats :

#!/bin/bash

source "$(dirname $(realpath $BASH_SOURCE))/config/letsencrypt.sh"

get_vhosts()
{
    local site=$1
    local vhosts

    vhosts=$(grep 'server_name ' $site | tr -d ' ' | sed 's/server_name//' | sed 's/;//' | grep -v '.onion$')
    vhosts=($vhosts)

    echo ${vhosts[*]}
}

count_char()
{
    local str=$1
    local char=$2

    echo "$str" | grep -oF "$char" | wc -l
}

get_main_vhost()
{
    local vhosts=$1
    local nb_dot
    local main_vhost

    nb_dot=$(count_char $vhosts '.')
    if [ $nb_dot -gt 1 ]
    then
        main_vhost=$(echo ${vhosts[0]} | cut -d . -f $nb_dot-)
    else
        main_vhost=${vhosts[0]}
    fi

    echo $main_vhost
}

get_email()
{
    local vhosts=$1
    local main_vhost
    local email

    main_vhost=$(get_main_vhost $vhosts)
    email="postmaster@$main_vhost"

    echo $email
}

get_lastchange()
{
    local vhosts=$1
    local pem="/etc/letsencrypt/live/${vhosts[0]}/fullchain.pem"
    local lastchange=-1

    if [ -e $pem ]
    then
        lastchange=$(expr $(expr $(date +%s) - $(date +%s -r $pem)) / 86400)
    fi

    echo $lastchange
}

transform_vhost_to_arg()
{
    local vhosts=$1
    local vhost
    local vhosts_arg=''

    for vhost in ${vhosts[@]}
    do
        vhosts_arg="$vhosts_arg -d $vhost"
    done

    echo $vhosts_arg
}

main()
{
    if [ "$1" = '--dry-run' ]
    then
        local dry_run=true
        shift
    else
        local dry_run=false
    fi

    if [ "$1" = '--force' ]
    then
        local force=true
        shift
    else
        local force=false
    fi

    if [ $# -gt 0 ]
    then
        local sites=$@
    else
        local sites=$(find /etc/nginx/sites-enabled)
    fi

    if [ ! -e "$LETSENCRYPT_WEBROOT" ]
    then
        mkdir -p "$LETSENCRYPT_WEBROOT"
    fi

    for site in $sites
    do
        local status='skip'

        echo -n "$site: "

        local vhosts=$(get_vhosts $site)

        if [ -n "$vhosts" ]
        then
            local email=$(get_email $vhosts)
            local vhosts_arg=$(transform_vhost_to_arg "$vhosts")
            local lastchange=$(get_lastchange $vhosts)

            if [ $force = true -o $lastchange -lt 0 -o $lastchange -gt 60 ]
            then
                if [ $dry_run = false ]
                then
                    certbot certonly $vhosts_arg \
                        --email "$email" -c /etc/letsencrypt/config.cli.ini \
                        --renew-by-default \
                        --server https://acme-v01.api.letsencrypt.org/directory

                    if [ $? -eq 0 ]
                    then
                        status='pass'
                    else
                        status='fail'
                    fi

                    if [ -f "$(dirname $(realpath $BASH_SOURCE))/config/${vhosts[0]}" ]
                    then
                        bash "$(dirname $(realpath $BASH_SOURCE))/config/${vhosts[0]}"
                    fi
                else
                    echo $vhosts_arg $email
                fi
            fi
        fi

        echo "$status"
    done

    if [ $dry_run = false ]
    then
        systemctl reload nginx
    fi
}

main $@

Un peu de bash velu, le plus compliqué étant de retrouver le nom de domaine racine pour renseigner l’email.

Notez, qu’afin de tenir compte des limitations, seul les certificats de plus de 60 jours sont renouvelés.

Une fois que vous avez votre premier certificat, vous pouvez l’ajouter à votre vhost :

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

Pour aller plus loin, vous pouvez également lire ce sujet.

Proxy

Nginx est un excellent proxy cache2. Dans ce cas il faut commencer par chercher le challenge en local et s’il n’est pas trouvé se rabattre sur le serveur primaire (dans le cas où l’on génère le certificat sur ce dernier). Enfin passer également toutes les autres requêtes via $primary_server_ip.

location /.well-known/acme-challenge {
    add_header Content-Type application/jose+json;
    root /tmp/letsencrypt/public_html;
    try_files $uri @proxy_pass;
}

location / {
    error_page 418 = @proxy_pass;
    return 418;
}

location @proxy_pass {
    proxy_pass $scheme://$primary_server_ip:$server_port;

    proxy_cache STATIC;
    proxy_cache_key $host$request_uri;
    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
    proxy_cache_purge PURGE from $primary_server_ip;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    add_header X-Cache-Status $upstream_cache_status;
}

Résultat

ssllabs A+ cryptcheck A+


  1. Les certificats joker ne sont pas à l’ordre du jour : https://github.com/letsencrypt/boulder/issues/21

  2. Si on omet la stupidité d’avoir réservé la commande PURGE à la version commerciale. Cela se contourne avec un bout de bash…