Terminologie: cert_master: Rechner, auf dem die Zertifikate angefordert und gesammelt sind. cert_client: Rechner, auf den die Zertifikate übertragen werden.

cert_master

Software besorgen/installieren:

apache, openssl installieren

Dehydrated:

git clone https://github.com/lukas2511/dehydrated

an passende stelle kopieren, /etc/dehydrated erstellen

cp dehydrated/dehydrated /usr/local/sbin/dehydrated
mkdir -p /etc/dehydrated

Apache konfigurieren

/etc/apache2/sites-available/letsencrypt.conf:Apache muss so konfiguriert werden, dass $domain/.well-known/acme-challenge/ auf das richtige Verzeichnis zeigt. Somit kann die Acme challenge erfolgreich durchgeführt werden.

Alias /.well-known/acme-challenge/ /var/www/dehydrated/                     
<Directory "/var/www/dehydrated/">      
    Options None
    AllowOverride None
    Require all granted
    ForceType text/plain
    Options -Indexes
</Directory>

Dann noch die config aktivieren und apache neu laden:

a2enconf letsencrypt
apachectl restart

Dehydrated konfigurieren:

folgende Dateien unter /etc/dehydrated erstellen:

config: Hier werden die Optionen von dehydrated eingestellt.
########################################################
# This is the main config file for dehydrated          #
#                                                      #
# This file is looked for in the following locations:  #
# $SCRIPTDIR/config (next to this script)              #
# /usr/local/etc/dehydrated/config                     #
# /etc/dehydrated/config                               #
# ${PWD}/config (in current working-directory)         #
#                                                      #
# Default values of this config are in comments        #
########################################################

# Resolve names to addresses of IP version only. (curl)
# supported values: 4, 6
# default: 
#IP_VERSION=

# Path to certificate authority (default:
# https://acme-v01.api.letsencrypt.org/directory)
#CA="https://acme-v01.api.letsencrypt.org/directory"

# Path to license agreement (default:
# https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf)
#LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"

# Which challenge should be used? Currently http-01 and dns-01 are supported
CHALLENGETYPE="http-01"

# Path to a directory containing additional config files, allowing to
# override
# the defaults found in the main configuration file. Additional config files
# in this directory needs to be named with a '.sh' ending.
# default: 
#CONFIG_D=

# Base directory for account key, generated certificates and list of domains
# (default: $SCRIPTDIR -- uses config directory if undefined)
BASEDIR=/etc/dehydrated

# File containing the list of domains to request certificates for (default:
# $BASEDIR/domains.txt)
DOMAINS_TXT="${BASEDIR}/domains.txt"

# Output directory for generated certificates
CERTDIR="${BASEDIR}/certs"

# Directory for account keys and registration information
ACCOUNTDIR="${BASEDIR}/accounts"

# Output directory for challenge-tokens to be served by webserver or
# deployed in HOOK (default: /var/www/dehydrated)
WELLKNOWN="/var/www/dehydrated"

# Default keysize for private keys (default: 4096)
#KEYSIZE="4096"

# Path to openssl config file (default:  - tries to figure out system
# default)
#OPENSSL_CNF=

# Program or function called in certain situations
#
# After generating the challenge-response, or after failed challenge (in
# this case altname is empty)
# Given arguments: clean_challenge|deploy_challenge altname token-filename
# token-content
#
# After successfully signing certificate
# Given arguments: deploy_cert domain path/to/privkey.pem path/to/cert.pem
# path/to/fullchain.pem
#
# BASEDIR and WELLKNOWN variables are exported and can be used in an
# external program
# default: 
HOOK=/etc/dehydrated/hook.sh
# Chain clean_challenge|deploy_challenge arguments together into one hook
# call per certificate (default: no)
#HOOK_CHAIN="no"

# Minimum days before expiration to automatically renew certificate
# (default: 30)
#RENEW_DAYS="30"

# Regenerate private keys instead of just signing new certificates on
# renewal (default: yes)
#PRIVATE_KEY_RENEW="yes"

# Create an extra private key for rollover (default: no)
#PRIVATE_KEY_ROLLOVER="no"

# Which public key algorithm should be used? Supported: rsa, prime256v1 and
# secp384r1
#KEY_ALGO=rsa

# E-mail to use during the registration (default: )
CONTACT_EMAIL=admin@lists.freifunk-mwu.de

# Lockfile location, to prevent concurrent access (default: $BASEDIR/lock)
#LOCKFILE="${BASEDIR}/lock"
# Minimum days before expiration to automatically renew certificate
# (default: 30)
#RENEW_DAYS="30"

# Regenerate private keys instead of just signing new certificates on
# renewal (default: yes)
#PRIVATE_KEY_RENEW="yes"

# Option to add CSR-flag indicating OCSP stapling to be mandatory (default:
# no)
#OCSP_MUST_STAPLE="no"

hook.sh: Funktionen, die ausgeführt werden, wenn verschiedene Aktionen durchgeführt wurden. Wir verwenden deploy_cert() für neue und verlängerte Zertifikate. Dort werden lokale (zuckerwatte) Zertifikate für den apache kopiert, Berechtigungen angepasst und apache neugestartet. Und es werden Zertifikate nach /home/cert kopiert, dieses ist das Transferverzeichnis für andere Hosts. Am Ende werden noch alte Zertifikate archiviert (dehydrated -gc).

#!/usr/bin/env bash
function deploy_challenge {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" 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.
}
function clean_challenge {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
    # 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.
}
function 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.
    #systemctl reload nginx
    #if [ "$DOMAIN" = "smtp.xxx.xxx" ]
    #  then
    #    systemctl restart postfix dovecot
    #fi

    echo "Renewed/Created: $DOMAIN at `date -d @$TIMESTAMP`" >&2

    # Copy local Certificates
    if [[ "$DOMAIN" == "zuckerwatte.freifunk-mwu.de" ]]
    then
        rm -r /etc/apache2/ssl/*
        mkdir -p /etc/apache2/ssl/$DOMAIN/     
        cp $KEYFILE /etc/apache2/ssl/$DOMAIN/  
        cp $CERTFILE /etc/apache2/ssl/$DOMAIN/ 
        cp $CHAINFILE /etc/apache2/ssl/$DOMAIN/
    fi


    chown www-data.www-data -R /etc/apache2/ssl/
    chmod -R 0700 /etc/apache2/ssl/
    apachectl graceful

    # Copy certs to cert home dir
    mkdir -p /home/cert/$DOMAIN/ 

    cp $KEYFILE /home/cert/$DOMAIN/
    cp $CERTFILE /home/cert/$DOMAIN/
    cp $CHAINFILE /home/cert/$DOMAIN/
    
    chmod 750 $(find /home/cert -mindepth 1 -type d )
    chmod 740 $(find /home/cert -mindepth 1 -type f )
    chown root.cert -R /home/cert/*
    
    # cleanup unused cert files
    dehydrated -gc

}
function 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).

    # cleanup unused cert files
    dehydrated -gc

}
HANDLER=$1; shift; $HANDLER $@

domains.txt: Hier werden die Domains (CNs) für die Zertifikate definiert. Der erste Eintrag ist dabei der CN = Common Name, jeder weitere wird als SAN = Subject Alternative Name eingetragen. Von diesen sind weitere 99 möglich (insgesamt also 100 Namen). Wir haben für jeden Host/jedes Gateway eigene Zertifikate um eine Angriffsfläche im Falle eines Zertifikatsverlusts zu minimieren.

#Hosts
#Zuckerwatte
zuckerwatte.freifunk-mwu.de dudel.freifunk-mwu.de einzugsgebiet.freifunk-mwu.de pad.freifunk-mwu.de rtfm.freifunk-mwu.de wiki.freifunk-mwu.de freifunk-mwu.de api.freifunk-mainz.de blog.freifunk-mainz.de einzugsgebiet.freifunk-mainz.de event.freifunk-mainz.de media.freifunk-mainz.de  mitglieder.freifunk-mainz.de pad.freifunk-mainz.de wiki.freifunk-mainz.de www.freifunk-mainz.de freifunk-mainz.de mainz.freifunk.net api.wiesbaden.freifunk.net einzugsgebiet.wiesbaden.freifunk.net media.wiesbaden.freifunk.net photos.wiesbaden.freifunk.net www.wiesbaden.freifunk.net wiesbaden.freifunk.net
#Glückskeks
glueckskeks.freifunk-mwu.de autodiscover.freifunk-mwu.de autodiscover.freifunk-mainz.de autodiscover.freifunk-wiesbaden.de lists.freifunk-mwu.de mailadmin.freifunk-mwu.de webmail.freifunk-mwu.de mail.freifunk-mwu.de
#Suesskartoffel
suesskartoffel.freifunk-mwu.de map.freifunk-mwu.de map.freifunk-mainz.de map.wiesbaden.freifunk.net map.ffmwu.org map.ffmz.org map.ffwi.org

#Gates
#Spinat
spinat.freifunk-mwu.de firmware.freifunk-mwu.de spinat.freifunk-mainz.de firmware.freifunk-mainz.de firmware.wiesbaden.freifunk.net spinat.ffmwu.org firmware.ffmwu.org spinat.ffmz.org firmware.ffmz.org spinat.ffwi.org firmware.ffwi.org
#Lotuswurzel
lotuswurzel.freifunk-mwu.de firmware.freifunk-mwu.de lotuswurzel.freifunk-mainz.de firmware.freifunk-mainz.de firmware.wiesbaden.freifunk.net lotuswurzel.ffmwu.org firmware.ffmwu.org lotuswurzel.ffmz.org firmware.ffmz.org ffmz.org lotuswurzel.ffwi.org firmware.ffwi.org
#Wasserfloh
wasserfloh.freifunk-mwu.de firmware.freifunk-mwu.de wasserfloh.freifunk-mainz.de firmware.freifunk-mainz.de firmware.wiesbaden.freifunk.net wasserfloh.ffmwu.org firmware.ffmwu.org wasserfloh.ffmz.org firmware.ffmz.org wasserfloh.ffwi.org firmware.ffwi.org 
#Ingwer
ingwer.freifunk-mwu.de firmware.freifunk-mwu.de ingwer.freifunk-mainz.de firmware.freifunk-mainz.de firmware.wiesbaden.freifunk.net ingwer.ffmwu.org firmware.ffmwu.org ingwer.ffmz.org firmware.ffmz.org ingwer.ffwi.org firmware.ffwi.org

Transferbenutzer anlegen

Dieser Benutzer wird verwendet als Transferbenutzer zwischen cert_master und cert_client

adduser --home /home/cert --disabled-login -gecos "" cert

SSH konfigurieren

/etc/ssh/sshd_config: Hier wird /etc/ssh/authorized_keys_local als zusätzliche Quelle für erlaubte public keys hinzugefügt.

AuthorizedKeysFile      %h/.ssh/authorized_keys /etc/ssh/authorized_keys_local

/etc/ssh/sshd_config: Hier wird ein chroot jail für den Zertifikatsuser eingerichtet, so dass ein geleakter private key kein manipulieren des cert_master zulässt. Die Permissions von /home/cert (wie gesetzt in hooks.sh) verhindern ein überschreiben der Zertifikate, internal-sftp erlaubt nur sftp über die Verbindung.

Match User cert
    ChrootDirectory /home/cert/
    ForceCommand internal-sftp
    AllowTcpForwarding no
    PermitTunnel no
    X11Forwarding no

/etc/ssh/authorized_keys_local: Hier kommen alle public keys von hosts rein, die Zertifikate abrufen wollen.

Cron

Das script in cron.daily läuft einmal täglich (wann wird bestimmt von Debian). Dann werden neue Zertifikate abgerufen und alte erneuert.

/etc/cron.daily/ssl_certs: dehydrated wird aufgerufen und stdout wird verworfen. Stderr wird über cron per mail weiter gegeben.

#!/bin/bash

dehydrated -c -g -f /etc/dehydrated/config > /dev/null

cert_client

SSH konfigurieren

/root/.ssh/config:Cron läuft als root, deswegen brauchen wir die config hier. $Hostname gegen den cert_client ersetzen, $cert_master gegen hostname cert_master.

Host $cert_master
        User cert
        Hostname $cert_master.freifunk-mwu.de
        Port 23
        IdentityFile /home/admin/.ssh/$HOSTNAME_rsa

/home/admin/.ssh/$HOSTNAME_rsa.pub: Den public key auf dem cert_master in die zusätzliche authorized_keys Datei eintragen.

sftp $cert_master muss ohne Eingabe funktionieren.

Cron

/etc/cron.daily/ssl_certs: Hier wird das Zertifikat des Hosts $cert_client abgerufen. Dann werden Permissions angepasst und Dienste neu gestartet. Andere Dienste erfordern hier Anpassung (z.B. mail).

#!/bin/sh

DOMAINS="$cert_client.freifunk-mwu.de"

rm /etc/apache2/ssl/* -r
for DOMAIN in $DOMAINS;
do
 mkdir -p /etc/apache2/ssl/$DOMAIN
 sftp -q -r zuckerwatte:/$DOMAIN /etc/apache2/ssl/ > /dev/null

 chmod 0550 /etc/apache2/ssl/$DOMAIN
 chmod 0440 /etc/apache2/ssl/$DOMAIN/*
done

chown -R www-data.admin /etc/apache2/ssl/
apache2ctl restart