Lain

Certificats

Gestion des certificats

Les certificats sont gérés avec dehydrated

/etc/dehydrated/domains.txt
chimrod.com
lain.chimrod.com

Renouvellement par dns

Le renouvellement inclus le domaine lain.chimrod.com qui n’est pas publique. Afin d’avoir un certificat sur ce domaine, le challenge dns-01 qui consiste à mettre une chaine de caractère dans l’enregistrement dns lui meme est utilisé.

/etc/dehydrated/conf.d/dns_gandi.sh
CHALLENGETYPE="dns-01"
HOOK="/etc/dehydrated/hook.sh"
/etc/dehydrated/hook.sh est un hook utilisé pour automatiser la connexion au registrar gandi et ajouter la clef demandée par le challenge.
#!/usr/bin/env bash

# Hook script mainly adapted from https://www.aaflalo.me/2017/02/lets-encrypt-with-dehydrated-dns-01/

# set -x

if [ -z "${API_KEY}" ]; then
    echo "Can't find API key. Please export API_KEY environment variable !"
    exit 1
fi

API_ENDPOINT="https://dns.api.gandi.net/api/v5"

deploy_challenge() {
    local HOST="$(echo $1 | rev | cut -d . -f -2 |  rev)"
    local DOMAIN="${1}" RECORD="_acme-challenge.${1}." TOKEN_VALUE=${3}

    # This hook is called once for every domain that needs to be
    # validated, including any alternative names you may have listed.
    #
    # Parameters:
    # - DOMAIN
    #   The domain name (CN or subject alternative name) being validated.
    # - TOKEN_FILENAME
    #   The name of the file containing the token to be served for HTTP
    #   validation. Should be served by your web server as
    #   /.well-known/acme-challenge/${TOKEN_FILENAME}.
    # - TOKEN_VALUE
    #   The token value that needs to be served for validation. For DNS
    #   validation, this is what you want to put in the _acme-challenge
    #   TXT record. For HTTP validation it is the value that is expected
    #   be found in the $TOKEN_FILENAME file.
    if [ ! -z "${TOKEN_VALUE}" ]; then
        echo "Creating DNS TXT field [${RECORD}] with value [${TOKEN_VALUE}]."
        DATA='{"rrset_name": "'${RECORD}'",
            "rrset_type": "TXT",
            "rrset_ttl": 300,
            "rrset_values": ["'${TOKEN_VALUE}'"]}'
        curl -s -X POST -d "${DATA}" \
            -H "X-Api-Key: ${API_KEY}" \
            -H "Content-Type: application/json" \
            "${API_ENDPOINT}/domains/${HOST}/records"
        # For debugging purpose
        # dig +trace "_acme-challenge.${DOMAIN}" TXT
    else
        echo "Something went wrong. Can not find token value to set for record ${RECORD}."
        exit 1
    fi
}

clean_challenge() {
    local HOST="$(echo $1 | rev | cut -d . -f -2 |  rev)"
    local DOMAIN="${1}" RECORD="_acme-challenge.${1}."
    # This hook is called after attempting to validate each domain,
    # whether or not validation was successful. Here you can delete
    # files or DNS records that are no longer needed.
    #
    # The parameters are the same as for deploy_challenge.
    echo "Deleting DNS TXT field [${RECORD}] for domain [${DOMAIN}]."
    curl -X DELETE -H "Content-Type: application/json" \
        -H "X-Api-Key: ${API_KEY}" \
        "${API_ENDPOINT}/domains/${HOST}/records/${RECORD}"
}

deploy_cert() {
    local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
    # This hook is called once for each certificate that has been
    # produced. Here you might, for instance, copy your new certificates
    # to service-specific locations and reload the service.
    #
    # Parameters:
    # - DOMAIN
    #   The primary domain name, i.e. the certificate common
    #   name (CN).
    # - KEYFILE
    #   The path of the file containing the private key.
    # - CERTFILE
    #   The path of the file containing the signed certificate.
    # - FULLCHAINFILE
    #   The path of the file containing the full certificate chain.
    # - CHAINFILE
    #   The path of the file containing the intermediate certificate(s).
    # - TIMESTAMP
    #   Timestamp when the specified certificate was created.
    echo "Generated files for ${DOMAIN} : key=[${KEYFILE}] - cert=[${CERTFILE}] - fullchain=[${FULLCHAINFILE}] - chain=[${CHAINFILE}] - timestamp=[${TIMESTAMP}]"
}

unchanged_cert() {
    local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
    # This hook is called once for each certificate that is still
    # valid and therefore wasn't reissued.
    #
    # Parameters:
    # - DOMAIN
    #   The primary domain name, i.e. the certificate common
    #   name (CN).
    # - KEYFILE
    #   The path of the file containing the private key.
    # - CERTFILE
    #   The path of the file containing the signed certificate.
    # - FULLCHAINFILE
    #   The path of the file containing the full certificate chain.
    # - CHAINFILE
    #   The path of the file containing the intermediate certificate(s).
    echo "Cert info: keyfile=[${KEYFILE}] - certfile=[${CERTFILE}] - fullchain=[${FULLCHAINFILE}] - chain=[${CHAINFILE}]"
    echo "Cert for domain ${DOMAIN} is still valid. Nothing to do."
}

invalid_challenge() {
    local DOMAIN="${1}" RESPONSE="${2}"
    # This hook is called if the challenge response has failed, so domain
    # owners can be aware and act accordingly.
    #
    # Parameters:
    # - DOMAIN
    #   The primary domain name, i.e. the certificate common
    #   name (CN).
    # - RESPONSE
    #   The response that the verification server returned
    echo "Challenge failed for [${DOMAIN}] - response was [${RESPONSE}]"
}

request_failure() {
    local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}"
    # This hook is called when a HTTP request fails (e.g., when the ACME
    # server is busy, returns an error, etc). It will be called upon any
    # response code that does not start with '2'. Useful to alert admins
    # about problems with requests.
    #
    # Parameters:
    # - STATUSCODE
    #   The HTML status code that originated the error.
    # - REASON
    #   The specified reason for the error.
    # - REQTYPE
    #   The kind of request that was made (GET, POST...)
    echo "Query [${REQTYPE}] failed with error: [${REASON}] - status code: [${STATUSCODE}]"
}

exit_hook() {
  # This hook is called at the end of a dehydrated command and can be used
  # to do some final (cleanup or other) tasks.
  :
}

HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|exit_hook)$ ]]; then
    "$HANDLER" "$@"
fi

Attention, ce hook nécessite la clef API permettant de se connecter à gandi. Celle-ci est renseignée dans la tache cron hebdomadaire :

/etc/cron.weekly/dehydrated
#!/bin/sh
API_KEY="${API_KEY}" /usr/bin/dehydrated -c
service apache2 reload

Utilisation dans apache

Une macro est disponible pour permettre aux site de mettre en place un certificat (celui doit avoir été créé avant)

/etc/apache2/conf-available/dehydrated_ssl.conf
<Macro dehydrated_ssl $domain>
        ServerName $domain

        <IfFile /var/lib/dehydrated/certs/$domain/fullchain.pem>
        SSLengine ON

        # enable HTTP/2, if available
        Protocols h2 http/1.1

        # HTTP Strict Transport Security (mod_headers is required) (63072000 seconds)
        Header always set Strict-Transport-Security "max-age=63072000"

        # intermediate configuration
        SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
        SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
        SSLHonorCipherOrder     on
        SSLSessionTickets       off

        #SSLUseStapling On
        #SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

        SSLCertificateFile /var/lib/dehydrated/certs/$domain/fullchain.pem
        SSLCertificateKeyFile /var/lib/dehydrated/certs/$domain/privkey.pem
        </IfFile>
</Macro>

La configuration peut etre regénérée à partir de ce site : https://ssl-config.mozilla.org/