From dce129afdc40255ddd7c8acff69d3fb9bcb1aadc Mon Sep 17 00:00:00 2001 From: Andrew Savinykh <658865+AndrewSav@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:20:10 +0000 Subject: [PATCH] Extract Traefik certificates with traefik-certs-dumper --- .github/workflows/traefik_v3.yml | 22 +++ Makefile | 45 ++++- rootfs/usr/local/bin/certs_helper.sh | 21 ++- rootfs/usr/local/bin/dumpcerts.traefik.v2.sh | 170 ------------------- test/share/traefik/v3/acme.json | 54 ++++++ test/traefik_v3.bats | 79 +++++++++ 6 files changed, 212 insertions(+), 179 deletions(-) create mode 100644 .github/workflows/traefik_v3.yml delete mode 100755 rootfs/usr/local/bin/dumpcerts.traefik.v2.sh create mode 100644 test/share/traefik/v3/acme.json create mode 100644 test/traefik_v3.bats diff --git a/.github/workflows/traefik_v3.yml b/.github/workflows/traefik_v3.yml new file mode 100644 index 00000000..4a944b67 --- /dev/null +++ b/.github/workflows/traefik_v3.yml @@ -0,0 +1,22 @@ +name: traefik_v3 tests + +on: + pull_request: + branches: master + +jobs: + # Run tests + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository and submodules + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Build docker image + run: make build-no-cache + + - name: Run tests + run: make traefik_v3 diff --git a/Makefile b/Makefile index d8dd0e4f..93a121c4 100644 --- a/Makefile +++ b/Makefile @@ -23,8 +23,8 @@ define dnsbl_probe -@docker exec $(1) sh /tmp/tests/dnsbl-probe.sh endef -all: build-no-cache default reverse ldap ldap2 sieve ecdsa traefik_acmev1 traefik_acmev2 clean -no-build: default reverse ldap ldap2 sieve ecdsa traefik_acmev1 traefik_acmev2 clean +all: build-no-cache default reverse ldap ldap2 sieve ecdsa traefik_acmev1 traefik_acmev2 traefik_v3 clean +no-build: default reverse ldap ldap2 sieve ecdsa traefik_acmev1 traefik_acmev2 traefik_v3 clean default: init_default fixtures_default run_default stop_default reverse: init_reverse fixtures_reverse run_reverse stop_reverse ldap: init_ldap fixtures_ldap run_ldap stop_ldap @@ -33,6 +33,7 @@ sieve: init_sieve fixtures_sieve run_sieve stop_sieve ecdsa: init_ecdsa run_ecdsa stop_ecdsa traefik_acmev1: init_traefik_acmev1 fixtures_traefik_acmev1 run_traefik_acmev1 stop_traefik_acmev1 traefik_acmev2: init_traefik_acmev2 fixtures_traefik_acmev2 run_traefik_acmev2 stop_traefik_acmev2 +traefik_v3: init_traefik_v3 fixtures_traefik_v3 run_traefik_v3 stop_traefik_v3 build-no-cache: docker build --no-cache -t $(NAME) . @@ -461,6 +462,46 @@ stop_traefik_acmev2: -docker rm -f \ mailserver_traefik_acmev2 || true +# acme.json as written by Traefik 2 and 3 (they share the format), generated +# with Traefik 3.6 against a local ACME test server. It holds two certificate +# resolvers, the case that used to leave the mailserver without a certificate. +init_traefik_v3: init_redis init_mariadb + -docker rm -f \ + mailserver_traefik_v3 || true + docker run \ + -d \ + --name mailserver_traefik_v3 \ + --link mariadb:mariadb \ + --link redis:redis \ + -e DBPASS=testpasswd \ + -e RSPAMD_PASSWORD=testpasswd \ + -e VMAILUID=`id -u` \ + -e VMAILGID=`id -g` \ + -e DISABLE_CLAMAV=true \ + -e TESTING=true \ + -v "`pwd`/test/share/traefik/v3":/etc/letsencrypt/acme \ + -h mail.domain.tld \ + -t $(NAME) +fixtures_traefik_v3: + # One in-place rewrite of acme.json (same bytes) has to produce exactly one + # reload; the tests count them. Postfix and dovecot must be up so the + # reload has something to signal. + @timeout $(WAIT_TIMEOUT) sh -c 'until docker exec mailserver_traefik_v3 nc -z 0.0.0.0 25 && docker exec mailserver_traefik_v3 nc -z 0.0.0.0 143; do sleep 2; done' \ + || { echo "TIMEOUT: postfix/dovecot not listening"; docker logs --tail 20 mailserver_traefik_v3; exit 1; } + @timeout $(WAIT_TIMEOUT) sh -c 'until docker logs mailserver_traefik_v3 2>&1 | grep -q "Watching /etc/letsencrypt/acme"; do sleep 2; done' \ + || { echo "TIMEOUT: cert watcher did not start"; docker logs --tail 20 mailserver_traefik_v3; exit 1; } + docker exec mailserver_traefik_v3 python3 -c "p='/etc/letsencrypt/acme/acme.json'; d=open(p,'rb').read(); f=open(p,'r+b'); f.write(d); f.close()" + @timeout $(WAIT_TIMEOUT) sh -c 'until docker logs mailserver_traefik_v3 2>&1 | grep -q "Updating SSL certificates and reloading"; do sleep 2; done' \ + || { echo "TIMEOUT: watcher did not react to writing acme.json"; docker logs --tail 20 mailserver_traefik_v3; exit 1; } + # Settle time in which a watcher reacting to its own reads would reload again + sleep 15 +run_traefik_v3: + docker exec mailserver_traefik_v3 /bin/sh -c "while ! echo PING | nc -z 0.0.0.0 587 ; do sleep 1 ; done" + ./test/bats/bin/bats test/traefik_v3.bats +stop_traefik_v3: + -docker rm -f \ + mailserver_traefik_v3 || true + fixtures_default: # Wait for clamav unofficial sigs database update (default) diff --git a/rootfs/usr/local/bin/certs_helper.sh b/rootfs/usr/local/bin/certs_helper.sh index 30110432..4b26c1d3 100644 --- a/rootfs/usr/local/bin/certs_helper.sh +++ b/rootfs/usr/local/bin/certs_helper.sh @@ -56,7 +56,7 @@ elif [ "$1" = "update_certs" ]; then mkdir -p "$SSL_MOUNT_PATH" mkdir -p "$CERT_TEMP_PATH" mkdir -p "$LIVE_CERT_PATH" - rm -rf "$CERT_TEMP_PATH/*" + rm -rf "$CERT_TEMP_PATH"/* NORMALIZED_CERT_PATH="$CERT_TEMP_PATH"/normalized mkdir -p "$NORMALIZED_CERT_PATH" @@ -74,7 +74,9 @@ elif [ "$1" = "update_certs" ]; then sleep 10 fi - if jq -e -r '.PrivateKey' "$ACME_FILE" >/dev/null ; then + if [ ! -r "$ACME_FILE" ]; then + echo "[ERROR] $ACME_FILE is not readable, check its ownership and permissions" | tee -a "$ACME_DUMP" + elif jq -e -r '.PrivateKey' "$ACME_FILE" >/dev/null ; then echo "[INFO] acme.json found with ACME v1 format, dumping into pem files" | tee -a "$ACME_DUMP" dumpcerts.acme.v1.sh "$ACME_FILE" "$CERT_TEMP_PATH" >> "$ACME_DUMP" 2>&1 elif jq -e -r '.Account.PrivateKey' "$ACME_FILE" >/dev/null ; then @@ -87,14 +89,15 @@ elif [ "$1" = "update_certs" ]; then fi elif jq -e -r '.[].Account.PrivateKey' "$ACME_FILE" >/dev/null ; then echo "[INFO] acme.json found with Traefik v2 format, dumping into pem files" | tee -a "$ACME_DUMP" - dumpcerts.traefik.v2.sh "$ACME_FILE" "$CERT_TEMP_PATH" >> "$ACME_DUMP" 2>&1 + traefik-certs-dumper file --version v2 --clean=false \ + --source "$ACME_FILE" --dest "$CERT_TEMP_PATH" >> "$ACME_DUMP" 2>&1 if [ -e "$CERT_TEMP_PATH"/certs/"*.${DOMAIN}.crt" ] && [ -e "$CERT_TEMP_PATH"/private/"*.${DOMAIN}.key" ]; then echo "[INFO] Let's encrypt wildcard certificate found" | tee -a "$ACME_DUMP" mv -f "$CERT_TEMP_PATH"/certs/"*.${DOMAIN}.crt" "$CERT_TEMP_PATH"/certs/"$FQDN".crt mv -f "$CERT_TEMP_PATH"/private/"*.${DOMAIN}.key" "$CERT_TEMP_PATH"/private/"$FQDN".key fi else - echo "[ERROR] acme.json found but with an unknown format" >> "$ACME_DUMP" + echo "[ERROR] acme.json found but with an unknown format" | tee -a "$ACME_DUMP" fi if [ -e "$CERT_TEMP_PATH"/certs/"$FQDN".crt ] && [ -e "$CERT_TEMP_PATH"/private/"$FQDN".key ]; then @@ -103,8 +106,8 @@ elif [ "$1" = "update_certs" ]; then DOMAIN_NAME="$DOMAIN" else echo "[ERROR] The certificate for ${FQDN} or the private key was not found !" - echo "[INFO] Don't forget to add a new traefik frontend rule to generate a certificate for ${FQDN} subdomain" - echo "[INFO] Look /mnt/docker/traefik/acme/dump.log and 'docker logs traefik' for more information" + echo "[INFO] Don't forget to add a new traefik router rule to generate a certificate for ${FQDN} subdomain" + echo "[INFO] Look $ACME_DUMP in the mail volume and 'docker logs traefik' for more information" exit 1 fi @@ -113,6 +116,10 @@ elif [ "$1" = "update_certs" ]; then rm -rf "$ACME_DUMP" RENEWED_CERTIFICATE=true else + if [ -d "$ACME_PATH" ] && [ ! -r "$ACME_PATH" ]; then + echo "[ERROR] $ACME_PATH is not readable, check its ownership and permissions" + fi + echo "[INFO] Traefik SSL certificates not used" if [ -d "$LETS_ENCRYPT_LIVE_PATH" ]; then @@ -151,7 +158,7 @@ elif [ "$1" = "update_certs" ]; then exit 1 fi - rm -rf "$LIVE_CERT_PATH/*" + rm -rf "$LIVE_CERT_PATH"/* cp -RT "$NORMALIZED_CERT_PATH/." "$LIVE_CERT_PATH" elif [ "$1" = "reload" ]; then diff --git a/rootfs/usr/local/bin/dumpcerts.traefik.v2.sh b/rootfs/usr/local/bin/dumpcerts.traefik.v2.sh deleted file mode 100755 index a0255ab6..00000000 --- a/rootfs/usr/local/bin/dumpcerts.traefik.v2.sh +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env bash -# Copyright (c) 2017 Brian 'redbeard' Harrington -# -# dumpcerts.sh - A simple utility to explode a Traefik acme.json file into a -# directory of certificates and a private key -# -# Usage - dumpcerts.sh /etc/traefik/acme.json /etc/ssl/ -# -# Dependencies - -# util-linux -# openssl -# jq -# The MIT License (MIT) -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# Exit codes: -# 1 - A component is missing or could not be read -# 2 - There was a problem reading acme.json -# 4 - The destination certificate directory does not exist -# 8 - Missing private key - -set -o errexit -set -o pipefail -set -o nounset - -USAGE="$(basename "$0") " - -# Platform variations -case "$(uname)" in - 'Linux') - # On Linux, -d should always work. --decode does not work with Alpine's busybox-binary - CMD_DECODE_BASE64="base64 -d" - ;; - *) - # Max OS-X supports --decode and -D, but --decode may be supported by other platforms as well. - CMD_DECODE_BASE64="base64 --decode" - ;; -esac - -# Allow us to exit on a missing jq binary -exit_jq() { - echo " -You must have the binary 'jq' to use this. -jq is available at: https://stedolan.github.io/jq/download/ - -${USAGE}" >&2 - exit 1 -} - -bad_acme() { - echo " -There was a problem parsing your acme.json file. - -${USAGE}" >&2 - exit 2 -} - -if [ $# -ne 2 ]; then - echo " -Insufficient number of parameters. - -${USAGE}" >&2 - exit 1 -fi - -readonly acmefile="${1}" -readonly certdir="${2%/}" - -if [ ! -r "${acmefile}" ]; then - echo " -There was a problem reading from '${acmefile}' -We need to read this file to explode the JSON bundle... exiting. - -${USAGE}" >&2 - exit 2 -fi - - -if [ ! -d "${certdir}" ]; then - echo " -Path ${certdir} does not seem to be a directory -We need a directory in which to explode the JSON bundle... exiting. - -${USAGE}" >&2 - exit 4 -fi - -jq=$(command -v jq) || exit_jq - -priv=$(${jq} -e -r '.[].Account.PrivateKey' "${acmefile}") || bad_acme - -if [ ! -n "${priv}" ]; then - echo " -There didn't seem to be a private key in ${acmefile}. -Please ensure that there is a key in this file and try again." >&2 - exit 8 -fi - -# If they do not exist, create the needed subdirectories for our assets -# and place each in a variable for later use, normalizing the path -mkdir -p "${certdir}"/{certs,private} - -pdir="${certdir}/private/" -cdir="${certdir}/certs/" - -# Save the existing umask, change the default mode to 600, then -# after writing the private key switch it back to the default -oldumask=$(umask) -umask 177 -trap 'umask ${oldumask}' EXIT - -# traefik stores the private key in stripped base64 format but the certificates -# bundled as a base64 object without stripping headers. This normalizes the -# headers and formatting. -# -# In testing this out it was a balance between the following mechanisms: -# gawk: -# echo ${priv} | awk 'BEGIN {print "-----BEGIN RSA PRIVATE KEY-----"} -# {gsub(/.{64}/,"&\n")}1 -# END {print "-----END RSA PRIVATE KEY-----"}' > "${pdir}/letsencrypt.key" -# -# openssl: -# echo -e "-----BEGIN RSA PRIVATE KEY-----\n${priv}\n-----END RSA PRIVATE KEY-----" \ -# | openssl rsa -inform pem -out "${pdir}/letsencrypt.key" -# -# and sed: -# echo "-----BEGIN RSA PRIVATE KEY-----" > "${pdir}/letsencrypt.key" -# echo ${priv} | sed -E 's/(.{64})/\1\n/g' >> "${pdir}/letsencrypt.key" -# sed -i '$ d' "${pdir}/letsencrypt.key" -# echo "-----END RSA PRIVATE KEY-----" >> "${pdir}/letsencrypt.key" -# openssl rsa -noout -in "${pdir}/letsencrypt.key" -check # To check if the key is valid - -# In the end, openssl was chosen because most users will need this script -# *because* of openssl combined with the fact that it will refuse to write the -# key if it does not parse out correctly. The other mechanisms were left as -# comments so that the user can choose the mechanism most appropriate to them. -echo -e "-----BEGIN RSA PRIVATE KEY-----\n${priv}\n-----END RSA PRIVATE KEY-----" \ - | openssl rsa -inform pem -out "${pdir}/letsencrypt.key" >/dev/null - -# Process the certificates for each of the domains in acme.json -for domain in $(jq -r '.[].Certificates[].domain.main' ${acmefile}); do - # Traefik stores a cert bundle for each domain. Within this cert - # bundle there is both proper the certificate and the Let's Encrypt CA - echo "Extracting cert bundle for ${domain}" - cert=$(jq -e -r --arg domain "$domain" '.[].Certificates[] | - select (.domain.main == $domain )| .certificate' ${acmefile}) || bad_acme - echo "${cert}" | ${CMD_DECODE_BASE64} > "${cdir}/${domain}.crt" - - echo "Extracting private key for ${domain}" - key=$(jq -e -r --arg domain "$domain" '.[].Certificates[] | - select (.domain.main == $domain )| .key' ${acmefile}) || bad_acme - echo "${key}" | ${CMD_DECODE_BASE64} > "${pdir}/${domain}.key" -done \ No newline at end of file diff --git a/test/share/traefik/v3/acme.json b/test/share/traefik/v3/acme.json new file mode 100644 index 00000000..0f57899a --- /dev/null +++ b/test/share/traefik/v3/acme.json @@ -0,0 +1,54 @@ +{ + "le-http": { + "Account": { + "Email": "admin2@domain.tld", + "Registration": { + "body": { + "status": "valid", + "contact": [ + "mailto:admin2@domain.tld" + ], + "orders": "https://pebble:14000/list-orderz/1044f32375680ef5" + }, + "uri": "https://pebble:14000/my-account/1044f32375680ef5" + }, + "PrivateKey": "MIIJJwIBAAKCAgEAtUImyJTXQwXZ7aelcBMVQKfmn2HcxbIJ+BL9W+Eg6Yun34OQEhpSiQAkMCo9py2e8Z4Kz4blZPWKtV2uiGlF2G+Cjo/HCDLntMJq+CczmO5xnq5j8ByEaxPDmgp49fSiaGv/7MySfKvQuMdERs1nfCg9gJ6NtGlYc63crN2v1PxgMjDgQicdJ1LxwWRbHLsZtn/KXB8Vvfb4j9ypQvnntht4hTaTRRzqtJlZvOg7TIzzaxcJgTZhpowT5B3ZY2Oy2EXBGiv48oLY26L2oFq4HaFsZ7f+xhqSjjTFVfyjYaPMGxLy/KHlD0OyPBx+xw3zQuGFgiI1AFgQUfyWrUC/x6xWRIulVZvUetMpLcruSsDiMOyksYrGuhbLhifJieNL5cYKdpD9+lFKBAA/59314YfIvLZ+cuxB5F7lJWzb5miPlsMEWGxVvAYenN0u7adoQZ1fJAh5mJsYo+J6EKo3T0RtB8T4SdKgmaS9LP43ntUEDgvdzwUIPKAomiUOoRY9+LLR8sMshHsJ2Hi6aoxaNqvXCc6AQGadAFeFMlZv3cnndxvfZLI+qpOKr5bF0pGbb3hxF2OOJXlqUCS3/lUNeE/j+DVamVQQ9dVc1AWJ5doH15qMuNvl8N2RkaVzzcZtrCz62Va2BAhWdy5V3qJ9UCVdMvNBJ3CZygfWSjdm9PcCAwEAAQKCAgAIwwnc6FT/iGjgdBorMMD2PyeGzNQ/OjwBIendZySQJZAX3eQ4+8zBkqAtM92HF013oIwlPAAawUPTQJQPRAd8TuegvJKCcvOAaWWCWWLFIcGcShHKqZY9Ftd9usPeH9U59v6IE9uA601LoVZLleJKvic68G8pKW59KQph0NWPFiQQSQyOJhXisL+++XruCJ1VlRfTHC2Q4T9itOf5tNrekrVguyKjVKKD2GXeG9vB945MvvBWrqpJwruzaz327/rVOYn8GeQO1QRmmZoSZlBAHm1/FgXaza4H8cUYQv4i2igjpubYD3tjIYKcJUR7mtEHip9Jix3Exn0DeZJPDbI3f8slbWPoVbAtsjjJSPFXZt28DrCNNXNTieqiRdAWVHdeauJCZK+wVqqlEcw7/56I151bA1KEjfI0ujv630hcEXliGrCRKzE7NDDuPG1IwJ5XusbzDpxmNUetovaLaamuBMFXWjJTIXdtnXKB/4qi8uIvkPcdO5BAIjXiqzvN9QXhbgNN3CDAkMzM5cVK5jbmLjro2lv3TzWuedmiETFu5X5RKUciIGRQ26QVBdj6ykG76nYfi47bZvLD07GcvqVmdxz0Tb4lL7Jk/ID2utV3Ync5P0DjVeoGHDZypjZi3ewNmJL4EHkmm8c0MzDqB3JH4ZTKvkLd42suEO7BKyNlAQKCAQEA78FcmTEQJW1KhoEsyhdV/DGtjg+lQEAYwbkMkZifnPq67fVD14RuB38EYxwsbQeLDhTWkSBcLYAXaihDwvdXdvMO0lVMGzrQnpicPdBmZAsET3PXXNpRO2rrR9irma6VjjJc0ilvhASXy1+H9sPZV1NdhBJwYJo7UoWv5LtozrS3M09QLaqTSlIBJY8N57IwRyuciqLksu33IXQdUmjinJSqziMT6gkV7PGaNw02iJfUj9MfniA31s9xtBWNKtTBUFIMccx86LEuUoixCTlm12BA4AbXhJAi4I+bAKqrOiAOn6st1y3G5KvMLnCAkgAnTGJsxQWGTctrnu8yKHBiowKCAQEAwYokBmMAKW6sOggLNP23VmAdhFnnmqTvIYCuj4klYaePgrHc1HVDXuKdo3+1T8GxxscUovswkPVNPmCrvlGKRTYZtkk9ynJSZor6XxW0r2WdiYApT94+MALRmkrmVzvSZ2mHG2ZXajNaqs93nGk6DhVXEE4Z5pKOCJ/vVSKjhpRe67vbhM/I++u+jSyZxiCPB32aF4QC4S3uGPeNrR4/JH+DSS/B86X26rlaY8Lmw3oOSWwHJoc5cuoo6G/7fMuyG7z1Pi99kEGp3NpXCtgTDmdJ7CDSkbr5caZB50mxtw6fw5GOxouLnBX7nThW7JXHkNgpfUt0qx+0DqeSFlodnQKCAQBDh2focgxFLmpORjXyWe5LEUb7+2YlGaifb9qda6qRUYGzImDQZCmFjP4UWEwfDX/aIH5q8/1ih3CQ4vRrBrWo+D7VeVbtWHyIHx3qvzcHuJ6nLk6bhZZZsPzCCMIUkDrpiT/hpfAV9CE5FBg8pd8inIV7fKIQY4CP9r2bJDoZ9oafOhiwNBo2VbCi/o8BEoxMj3kRB155E1OHJahYnNgvGCSf2Ptz8MkvKsGv/dW9VV52gBWouRRhOZ6iBWv0g2ZLlECTEPChrb4JWxzaJ0PTJ/Y9409ytBJOw8P5xRhjT8IusM1BjVA2+jiRX/impspq1lIikDLK2J/gbSjogPcXAoIBAF9mqcVgLac+v7THXEbdve7c6A/ihVDsdlfMZ7+nOf7+Krag2Br+RpxSFqSOgiBzeR9j6N9B9LmvwUi7PE1PQlpFhb0eM1BEtCuJ3ErclzlSXKp3u2V8RqlB6nIk85ASk++XzBqMgxB9ifbi17IJCK4ChO0roobOvSaxN7d+oJ7pKjUHX/kCt66+kSpjztUkHXzyw+9NimHXUyOJe5PXcgaauGtuGCvzJlijz1OXskqvMrWsg4XRaFcl585TzOLCQeR8a2Mg48MlKwcLtZZJ1nLmJUE5GLLvOKB19Mx+vhRfKA+uA3QJO2NQTrw8FlT+q+XRN/pkJHlp4kbLAS71ct0CggEAb6lqT9RekSkbNP9aj6zhhkZSD7K0SyAhChhfO3JmmUpim/5rMDGuiJ+qhIUvnmrzcemELjiiNTE8ZiTke4TGgcRf8NIaDeXbHf3oH2Y2LjjX7BrbAJh20reJHYsfO1uF5a+U+7DZNPH7dRRoBMqQ25XQt9pT7zkzcVBB1iycNF3Fe/z8IFlzKMvZIXVA3Rb202H2DTlccMaLACi7z2CuC+UER2nTIOKLzQz4/uqVDvzs2a8rj/yBluoIc+DyXTauDljEmiryorxbzGPEOQFry24rs3OoXTIA/gng8B+DNdKhFBPnSPJQBT+BAGW6h2X8NrRt3mzC2JGBQEcx5jXynQ==" + }, + "Certificates": [ + { + "domain": { + "main": "spam.domain.tld" + }, + "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYVENDQXdTZ0F3SUJBZ0lJUmJUU1VWeXRQREV3Q2dZSUtvWkl6ajBFQXdJd0tERW1NQ1FHQTFVRUF4TWQKVUdWaVlteGxJRWx1ZEdWeWJXVmthV0YwWlNCRFFTQXdOV1ppTmpNd0hoY05Nall3T1RFMk1qQXpOVEl5V2hjTgpNall3T1RJeU1qQXpOVEl4V2pBQU1JSUNJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBZzhBTUlJQ0NnS0NBZ0VBCjY1TUVtNkNWcHp1OUJZaStLVzRkZkwvU21mWU03anJ5elVOWnZabEt1Q3NDYTlTengzejNNZEhoRUtXUUgyNkMKTTVzSXcwTG95T1k2aC8rYUViampkS0RldDgxUWpSNDZ1NGtkeVY2MkgwenhkaVJ0OGFiN05CVThyWVZLRDVSRApqb1Q5WjBWQU5LeUxseG1naUhBeE9xK0RTSVJscjFuNTlQOVV6bk5Hc21nbnRTNkVYQjVZL3pxTzFMVXh4cE02CjhGVXNlN0FXZnI2b0REN0x4VTlJQ1A4WVNNSFdDbjNFQmZUWTNNYWc2cjNNTlNjVGJES0xLTnZkc2N1MUtCUWwKMnUyM2p5cS83M2UvbmthYkdVRGNlSzBXbjVVNjUrOEI3dnMrSzU3emc5OENwSWJNcGJQWkdJVnJTM0N0Z3pWagpWc0ZOV3FOT0JpL3E4T0k2M3JUSzd5bnBFcG94eDIvcndYK0ZESU9QY3UycnRtN0hWQ0xoakpqMFZyakJJTzBMCmpmYSs2anZWeXJLTUlqOG8rK2FLVVdDYy90TkNROHdFUjI3eUZZNkV3NVdnMzREVGpidXhKeXNFdnkrY2JScWoKMEErK1F5WC9tWndEWjJUck5VUXh0MnBxN0QxWW4yd1J1L3NrZnVJUkx5KzFHZjh6bk9KdFoyV0tYaitHNnFQTQowbXB4M0VPQXZiTVJsV3ByT3JlMjJHK0RLR0d6ODBabUZFV3VvTUFocG9jTHZUVTVKc2pUai8ybS92OFdrd1lCCmNmSDdPM2JydENMbStQSDhJQXc0ZXRPQ0huNUNJU3NjOEF5T3dtZUQvRy9CS3o0dEx2aklqWkVUOTh3a3czQWIKa3d4dWtKdVoyT1JLaUY4amViUGpnSmplZW1BbE1VL25kL25OY0ZDU21oMENBd0VBQWFOMU1ITXdEZ1lEVlIwUApBUUgvQkFRREFnZUFNQk1HQTFVZEpRUU1NQW9HQ0NzR0FRVUZCd01CTUF3R0ExVWRFd0VCL3dRQ01BQXdId1lEClZSMGpCQmd3Rm9BVThDYi9DeEVMK2ovOWg2c09tSzZueXNHUVFCc3dIUVlEVlIwUkFRSC9CQk13RVlJUGMzQmgKYlM1a2IyMWhhVzR1ZEd4a01Bb0dDQ3FHU000OUJBTUNBMGNBTUVRQ0lDZkNXQnk2MmJ5WlJ4cGNmQjBFODlNdgp4b3ZsSnd4bTR6Rm0rTEQ0aGd5MkFpQkxFbk9DN1RGR0U0TUprUGlUVCtzZml5UlVaY2tLWXI2NTl5WmpYL2hBCitnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQnR6Q0NBVjZnQXdJQkFnSUlRWlFPM1JkSkhFY3dDZ1lJS29aSXpqMEVBd0l3SURFZU1Cd0dBMVVFQXhNVgpVR1ZpWW14bElGSnZiM1FnUTBFZ05EUTFOVFV5TUNBWERUSTJNRGt4TmpJd016VXdPRm9ZRHpJd05UWXdPVEUyCk1qQXpOVEE0V2pBb01TWXdKQVlEVlFRREV4MVFaV0ppYkdVZ1NXNTBaWEp0WldScFlYUmxJRU5CSURBMVptSTIKTXpCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkgrcDNqckh3UG5WWHNEcFl4b2NJUHhETEZIMQp6S3RwWFBNY3ZOeStUZ1FUTWYwRldRM1pWY3FOa1FuaCtnQ1dob3dTR1U5amFnNGtTVy9qbFY5ekgwV2plREIyCk1BNEdBMVVkRHdFQi93UUVBd0lDaERBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEUKQlRBREFRSC9NQjBHQTFVZERnUVdCQlR3SnY4TEVRdjZQLzJIcXc2WXJxZkt3WkJBR3pBZkJnTlZIU01FR0RBVwpnQlJZNmFBdS94MXV6UituL0xXR3BIMXNXWUZzN3pBS0JnZ3Foa2pPUFFRREFnTkhBREJFQWlBZCtoRUE3N3FhCjZPckRvNHNSYndBejFndkIyaHFBSS9kSkdEL2JOSU9MeXdJZ2FDWUNTRFByc1BIQUN5YkF6VU5PQnhkSUNGLzEKdGk3S2l3bGdLTTNKT2d3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + "key": "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRRHJrd1Nib0pXbk83MEYKaUw0cGJoMTh2OUtaOWd6dU92TE5RMW05bVVxNEt3SnIxTFBIZlBjeDBlRVFwWkFmYm9Jem13akRRdWpJNWpxSAovNW9SdU9OMG9ONjN6VkNOSGpxN2lSM0pYcllmVFBGMkpHM3hwdnMwRlR5dGhVb1BsRU9PaFAxblJVQTBySXVYCkdhQ0ljREU2cjROSWhHV3ZXZm4wLzFUT2MwYXlhQ2UxTG9SY0hsai9PbzdVdFRIR2t6cndWU3g3c0JaK3ZxZ00KUHN2RlQwZ0kveGhJd2RZS2ZjUUY5TmpjeHFEcXZjdzFKeE5zTW9zbzI5Mnh5N1VvRkNYYTdiZVBLci92ZDcrZQpScHNaUU54NHJSYWZsVHJuN3dIdSt6NHJudk9EM3dLa2hzeWxzOWtZaFd0TGNLMkROV05Xd1UxYW8wNEdMK3J3CjRqcmV0TXJ2S2VrU21qSEhiK3ZCZjRVTWc0OXk3YXUyYnNkVUl1R01tUFJXdU1FZzdRdU45cjdxTzlYS3Nvd2kKUHlqNzVvcFJZSnorMDBKRHpBUkhidklWam9URGxhRGZnTk9OdTdFbkt3Uy9MNXh0R3FQUUQ3NURKZitabkFObgpaT3MxUkRHM2FtcnNQVmlmYkJHNyt5Uis0aEV2TDdVWi96T2M0bTFuWllwZVA0YnFvOHpTYW5IY1E0QzlzeEdWCmFtczZ0N2JZYjRNb1liUHpSbVlVUmE2Z3dDR21od3U5TlRrbXlOT1AvYWIrL3hhVEJnRng4ZnM3ZHV1MEl1YjQKOGZ3Z0REaDYwNEllZmtJaEt4endESTdDWjRQOGI4RXJQaTB1K01pTmtSUDN6Q1REY0J1VERHNlFtNW5ZNUVxSQpYeU41cytPQW1ONTZZQ1V4VCtkMytjMXdVSkthSFFJREFRQUJBb0lDQUFLSk14Ly93d3lwVW5nWVNEL0c3UzlXCkcwekgvc3lqdXRCbDNMWHpUQUFlaW1paHdndXdOek1FbU9FbWZ4NHZnTnhRVzNCS3JKblpES0ZCS2FYdU1SaWsKMExEMjExcWxqQ2xQcVEvdFlwV0VyUWJDRHFMdTByNTFTaEZ1SVdWNGlaK2pvOXkwUEtRMGhVNHJtdlJORE9LZQpXQkgwUnVuejFJS0VLeVFMenFPR0JvTm5URXAza3lxd05UQnFBTzZyMEJNZFA1elhRTzY5VzY3Q0VFeC85WDBQClRmSWVrQkROMk1uZG9HWUVCMkJ2aVo2NjNIVXFPQ0JJK2FKNzJHZmxpNkRITlZZbnRpZzhTRVpLYVVvRXpqckkKVmptQ2tEOUY3ZVhtTTMyaHVPcGZBTGJIc3JWSDhkclhPTmYzanc2ZUNRTTZrbndhelBDNUhGVGFncHE2SklNWQpUVlNjN3FtcFdJVHJmOElrdVVnUGZCcVZ2cTl1V3YyWklCSk8rdVVzV05FSGJ4a2xKY004MWNqellza0dkMzVTCjdQZE5WdE1JZmFpY280S3BDQzNhUUI1N1N6NEZnVGZsbGdmbkIwclQyZ0VMcWYva0RLTWMwU1ZBUmoxcm95bEYKYW5kOUZhZFliN0FEMG50bVYxQU9zZ2FMMEE2ZXhkcEtTZkNiQ3BGR3kwT1JVQTFPbUx4dzFvR3RVU1puQVdSQwpHdDF0eXhmdFdjaGtra3JvSTJUckhGY3g3NWY0TUhoV2NaYUQ3bEJjSnVCNC8rR1NxejYwRlV6UEJVbjRINW9DCk5QOTBvT05uZ1cxRjJ0S0RIUnN0b0cveGRzc0xid2xxVlYwS0xsb2xuVk94bDNRcEVBOFZVeW9lL0lYaVlGR3AKNHlVNE96TjVXQ0JyMUlFODZhZTdBb0lCQVFEM1U2cjRvekV0YUZEbUhXUlk5cEMzY1U2VUdxRG82SXJlSy9ZLwprRzYwZ09LdTdIbWNLRWpYbEd3N2d5Wm9zNVRKYlpLTGRTR3M0cFJxdGNKQ2J3N2xxcVlKZUtCTENmcHI5YWtUCk80ei9PWmRYM0x3MGI2OW8rZWxDdVFhVXhhRDdIN3JTUEtBQWxKblNvYzlIUWtZbDFGTmNZMkNDQzY4dFpRM08Kc3RQSklTSTJGdDBGWktvNEpwSUF2OXBHZmJTemhXRENrWWtwbXRielhNMVpMWmFmTG9GTVJhSUpFbHNSNzVZaApzV243c0VVRGhqZUNiclEvcFFqTlZCNWpKY0s5cmJzNGFuekkyNHBQcUw1NnMxVVliTEhPNnRNS05mUVZ0dW5ICndUbjdVYlB1OUZDc0F1UFBrSXQ2T21sVm5BM3Y0YzM1Z095MWp5QXcrL0xhMFZ6ekFvSUJBUUR6MWRnRitGUGEKVGwySzAyQWxBQVBjaXBLNkdhMDVkUEQrOEg5TVRYejVPY3VxZVlKL0RiVXFQcnA4OEoyeldyYWg5MGNvWGlTNgpqSVoxT1NIdXRLNkxVcXVlU0NBbXVTd1hjVDY2NFFTMFBjN2g5T3VlMGJjNzFXV3JYSkVRMGtSUEdCQmhGRGk0Ck9PN1BQMHRXVk5mN0p1YjlUM0dZLzRyUFlGRmF6L3VDSGdwWkpJWXNmc0xsek9Qb1h4aUorUCtoZFNaRmY4V1QKQXRuRXZGV0RncVdWdzF3YXZYeDdZSDV1amoxUWZCaTVVQXF2amFueVlJK2syQVZ2alZiM0VDR1JCUFBUOWI5MApwQzAxNFB2SXFzOWFYMVc2ZDdKa2VVRDloN3k0T2cwd0NSUk4zY09tTmpPSkhaMExJK1VLN1oveGNwQ3Z5WkdECmNyeFFNWEZWZjdDdkFvSUJBUURYeENCRjVXakRxbHZaUlE4RUphYSt3cFRBMURKSXZvWlhxSmFkZ1kxeTMzWGoKRU41UEF6MzVCTDBmZGgwTi9venc1U3lSaVZBbG1VM05nbC9Fd1VxSHhrWXZ3UkptSklCWnF1a2pwREl2ZnJpdgpxQnh3eW5hRzZHODlERm5YZEVvcWl0WGJENVN1clBVOU1uaTRPL01pa1lkZjlEcC9lVjRNQmpyemRrTVpGbFRZClJ6M3V0SkZqaHhLY29NTDErRGlxM05NN0t1ZzBhYWJZakwrQTZ5cWFHR0lvZmg4TU52MUlPbklKVnRPVkJZUlAKdk1tMGVvdTVjbXFwWnBkVCs5OVd4cmVzVWdvbTJKQmpTbDFTZ2h6Y3dBYW04Mm5GWUtoZVFVTFdSNVlqMEhWZgpJN2sxTnhnelBuRFA0eUY5MURTOGI0dlgvVWVFWlNhNlZUTGEvd2ROQW9JQkFEc09neGpIRVlBaFZOU241M1J1CnNZQzc2MWU1bWxQOU45VjNhRTJSd3dBVitIY2NvUGVtMlVjWEZGSG9jVG1jY2ZsK2k3U0NRTUVTQWdyb1FJaXgKS0VsRVI1NjVkMWdKcDEweUozQlp5YXJpQVhhR3ZINmw2b3hLWGlxSDYvYTRCcVZsZmFvVm5MczV4dFZaVGlUbAp2YjN0cVNxWEVEdHZRUzZDOUpGYktGNVdha2syNkd4a2MwMzNOd2Z6NkZ5YWUwZWV1UnRDSEJkRUxIeFM1UjlLCkNGTGlreTlDejZDT09YRUxjRnhMQWkzMzdlNmhFQlZOUDkwOEdPend2YXh2TXNJT0FnUkhWQXlyS3V3T3hzek8KV2hSWVhNb2g3WW9hT0J4dndNeVordEcwa1Q5ZXcrRkE1WENEUktNTEdaRnhCamtUZDJtc0YxdFA4YjQyaTIyYQoxSzhDZ2dFQUMxRnZGclRSMmlrdFZzQU1xVGppWHkzbHpKYTA2ejhJV3p4QlZRMFlvZmVOL2hwSldRYW82NU5VCjhzYVl6ZU5sS0tTZldWSit5MVl2aFRrNHBEb1BRcFdTUGpNd3JEbDRIRTNrOS9XVFpUbWt5OW55cDlEVHpvaDgKcE5abEZGV2Qwb3lQVzJBL2s0Nkx0eWt0TklsZVk2MjVMMUJZT2RjazdlREx0dkxSU0c5TDIvQjUrdkttdmhIKwp2a3NVYVYrc1pHbkJrZ3Vxb1pmTng4QmxZL3JwbUYra1BsYkp4dmIrQ09jdStKcDdEWkFWYnRJVWR1a3Rva3MzCkYxcWNXVms1MUhTZ1hHclFlb0M1K1dyM3RuYTRjWEd3ejhoL2pHaksxTm5YM0tzMUtxdHFrN1FXYjNXTWs1bmYKOGx4SVBQMFZBSk16K2Jwb0Z0UmxQcDFRNG12eGt3PT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=", + "Store": "default" + } + ] + }, + "le-tls": { + "Account": { + "Email": "admin@domain.tld", + "Registration": { + "body": { + "status": "valid", + "contact": [ + "mailto:admin@domain.tld" + ], + "orders": "https://pebble:14000/list-orderz/ddb33826df18acb" + }, + "uri": "https://pebble:14000/my-account/ddb33826df18acb" + }, + "PrivateKey": "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" + }, + "Certificates": [ + { + "domain": { + "main": "mail.domain.tld" + }, + "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYakNDQXdTZ0F3SUJBZ0lJWHBOMk1SM2FuT3N3Q2dZSUtvWkl6ajBFQXdJd0tERW1NQ1FHQTFVRUF4TWQKVUdWaVlteGxJRWx1ZEdWeWJXVmthV0YwWlNCRFFTQXdOV1ppTmpNd0hoY05Nall3T1RFMk1qQXpOVEU1V2hjTgpNall4TWpFMU1qQXpOVEU0V2pBQU1JSUNJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBZzhBTUlJQ0NnS0NBZ0VBCnkrUHpOQjhGT1RoQmZMOWllaThOR2JDYUIwUlI3aDRRMWJjSXVpM3FuMThaMWVsckhoSWZaQ3BVNnY2OEZqTHMKTzFpN3VHOWl5Q2RMM2lYWlpXYjIyU0pDanlqbXdvRlNWbEdZenRWRk8xWGxBdyt1Zyt3UnJlQVBqTmNxR0ZrcgpjTStXbDhBNWpRQThaUXhrdWNkY2JCR3k5ZUdhbjF2MWpvSnhCeW0weDZnb3ZqMU94TFlCMTljVWtweTU0SFJsCnZBL3lQU1E1U25XWnczTHJwMWlpRVJJa1lWbzRad0swejFENm9rWXN6czh0eGVCTmVLelM5QVZVZ0N5QmxlbjkKSXdSK3VWMkZRcHR4UlhWTVBIRXhldzNSZitNeHlBRFBYMlZYREk4bWY2bTRoUURVeExYN3dwWVlZdmQ3ZzRDWApHK2tOQXNNTCtRZXlqYnZjMXVaMEZnalcxT0Y4VGVxb1h0b3NNRUpqVFpZWjY1WHBiWjREMHMrM2twTEdPSC83CktzNG9kNG5zMWRrVGRISUZCaldnRDJWKzM1dWxpWXF2YXhsUzVoRFVNTW1FU0dkMzlOc1hJVWxYdUFxaVhTQlEKNFowbWhyVmsyOENyallmUmhNVVdEK3M3UmpyMG1ZclFMa1pNZXhwVE55YzFuSURWWkF5NkprWlZybTBKbDZCeQpGSVk1OUJUeHlZRHJ5WnMwVkxETVBLWXpyRFN4QzRzT1p5MHNPMjZJaWF4UytRaEswbWQ0Q3pQWGZ3ZjdxTTh4CnptUFk4QStrZExNcDFvbmVJdUlUTXl1SndJbDFHdnoySkgzV3FSQWVVM0dFYzNER3d4SnRLWVZ3RFBiRkgvUGcKSzZBSlhlWGMwalJwaS9jOUtMZjZRV1NmbjQ2QkkxYlR1blJBMER3aU14Y0NBd0VBQWFOMU1ITXdEZ1lEVlIwUApBUUgvQkFRREFnZUFNQk1HQTFVZEpRUU1NQW9HQ0NzR0FRVUZCd01CTUF3R0ExVWRFd0VCL3dRQ01BQXdId1lEClZSMGpCQmd3Rm9BVThDYi9DeEVMK2ovOWg2c09tSzZueXNHUVFCc3dIUVlEVlIwUkFRSC9CQk13RVlJUGJXRnAKYkM1a2IyMWhhVzR1ZEd4a01Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lFZWVLcHdkVmZVR0VUcTZVdktzZ1ZpdQovUjEycmE1RGc1ZUIxOVlkRDJ5UUFpRUEyNjBjUkR3SmVxMnd4b2pVMHNtYVd3b09rMi9jZUhLMHQ2ZU5OUjlPCkZtdz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJQnR6Q0NBVjZnQXdJQkFnSUlRWlFPM1JkSkhFY3dDZ1lJS29aSXpqMEVBd0l3SURFZU1Cd0dBMVVFQXhNVgpVR1ZpWW14bElGSnZiM1FnUTBFZ05EUTFOVFV5TUNBWERUSTJNRGt4TmpJd016VXdPRm9ZRHpJd05UWXdPVEUyCk1qQXpOVEE0V2pBb01TWXdKQVlEVlFRREV4MVFaV0ppYkdVZ1NXNTBaWEp0WldScFlYUmxJRU5CSURBMVptSTIKTXpCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkgrcDNqckh3UG5WWHNEcFl4b2NJUHhETEZIMQp6S3RwWFBNY3ZOeStUZ1FUTWYwRldRM1pWY3FOa1FuaCtnQ1dob3dTR1U5amFnNGtTVy9qbFY5ekgwV2plREIyCk1BNEdBMVVkRHdFQi93UUVBd0lDaERBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEUKQlRBREFRSC9NQjBHQTFVZERnUVdCQlR3SnY4TEVRdjZQLzJIcXc2WXJxZkt3WkJBR3pBZkJnTlZIU01FR0RBVwpnQlJZNmFBdS94MXV6UituL0xXR3BIMXNXWUZzN3pBS0JnZ3Foa2pPUFFRREFnTkhBREJFQWlBZCtoRUE3N3FhCjZPckRvNHNSYndBejFndkIyaHFBSS9kSkdEL2JOSU9MeXdJZ2FDWUNTRFByc1BIQUN5YkF6VU5PQnhkSUNGLzEKdGk3S2l3bGdLTTNKT2d3PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==", + "key": "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRZ0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1N3d2dna29BZ0VBQW9JQ0FRREw0L00wSHdVNU9FRjgKdjJKNkx3MFpzSm9IUkZIdUhoRFZ0d2k2TGVxZlh4blY2V3NlRWg5a0tsVHEvcndXTXV3N1dMdTRiMkxJSjB2ZQpKZGxsWnZiWklrS1BLT2JDZ1ZKV1Vaak8xVVU3VmVVREQ2NkQ3Qkd0NEErTTF5b1lXU3R3ejVhWHdEbU5BRHhsCkRHUzV4MXhzRWJMMTRacWZXL1dPZ25FSEtiVEhxQ2krUFU3RXRnSFgxeFNTbkxuZ2RHVzhEL0k5SkRsS2RabkQKY3V1bldLSVJFaVJoV2pobkFyVFBVUHFpUml6T3p5M0Y0RTE0ck5MMEJWU0FMSUdWNmYwakJINjVYWVZDbTNGRgpkVXc4Y1RGN0RkRi80ekhJQU05ZlpWY01qeVovcWJpRkFOVEV0ZnZDbGhoaTkzdURnSmNiNlEwQ3d3djVCN0tOCnU5elc1blFXQ05iVTRYeE42cWhlMml3d1FtTk5saG5ybGVsdG5nUFN6N2VTa3NZNGYvc3F6aWgzaWV6VjJSTjAKY2dVR05hQVBaWDdmbTZXSmlxOXJHVkxtRU5Rd3lZUklaM2YwMnhjaFNWZTRDcUpkSUZEaG5TYUd0V1Rid0t1TgpoOUdFeFJZUDZ6dEdPdlNaaXRBdVJreDdHbE0zSnpXY2dOVmtETG9tUmxXdWJRbVhvSElVaGpuMEZQSEpnT3ZKCm16UlVzTXc4cGpPc05MRUxpdzVuTFN3N2JvaUpyRkw1Q0VyU1ozZ0xNOWQvQi91b3p6SE9ZOWp3RDZSMHN5blcKaWQ0aTRoTXpLNG5BaVhVYS9QWWtmZGFwRUI1VGNZUnpjTWJERW0wcGhYQU05c1VmOCtBcm9BbGQ1ZHpTTkdtTAo5ejBvdC9wQlpKK2Zqb0VqVnRPNmRFRFFQQ0l6RndJREFRQUJBb0lCL3pyZTBQSGNLKzRrWElud3JvQ3dWNVM0CkxXZzNsOWFUcWR0U0pBRmNQbnFDQTlyaTg5RkZneHBZeis3VnhkODJWMnM5R3VVeHU4VUNwMTJpYXVJRldIWkUKV2JoMG5jeEV4SGNyb3RXZmtrZEFKazZzcUJERW0yUU0rZzYzeTZmc25BR2FEV2tIY2luaWhkSkJUZDhEL2M5WgpZc3kwS3R5elcxbDVuK3NjOFFVUGlCZ0RjdGpPNzhPRFpVQVF3cmlxcGR2MnRmMGpCcWMzakR6VStkQ01mZ3FLCkRJMVV4WWtobEQ1dzRQSnRLczFPQVNtWU5aNG9rWG5MR3lIa09iYUtFWFhNODZJTDRzSTdmellmWGRSOWRvcGcKeVJuMG1UREJJK3VFcW1xMzdqckNoYVRGS2MzYmI5ZDMvaFhoMjF5OHE0VmVieE1QeEh2V2xjRWRPS1IyM0pGcApGNGxIVnZjQlZNZC9KejhJcWdmTU1waDRGU2N5TmNQczU0Z2ZxMGpGNGZ5OTBkbWhnR2RuUGh6Y3ZTTnJoRm1XCjVudm8vZU1DejZvb2FDdC9tQWdyOUhUaXQweWdBK0tqMFRDeCthU05HMGs2TVhmcXF3Y1VaQ0g5YzhYd0x4a2cKU1o1cWRCZUVpaHg1NDhiYzI4YTRNNFlFK1A3U2M5UFpOMndNZXJWK1BKdTJadzBydmlzc1UxYXVQTVc0Ylh0ZgphSWNCZU9zcTBjSFVxTHJzK2ZEeTdXQ2tKYkpHV3hibkFoNVZ3ZFlIZTVvRW5yNWZKdEZCVVN4TkM5a0UraE1BCjl3RHIrOFZNNS9hVEJHbk5jbWNENUZ6RUx3V2h6SW96aEdkYVo5VEcyMXRsM0t3RTBOc1hBWDJQRFo5ZGhFd0YKdlFvN2UzQ0I3eFFRajNDMEZwRUNnZ0VCQVBvbWxiNkMvTlhEbzBkOWNyR3k3akhjc3NtZFZzQjl3ZzF4TGY2aQpUU2xudlE4R2ovSE9HdkJlU2xJN2V3eENRSkFNVCtlWEhmMTIyb2RZcjZFbi9OQXBtK3BjZ3MwUU5aTUlONmtrCldIb2piVlZrbTJSMTBlb0xsSWRkSmFLU0ltS1hoZnZvV2xJSFB6Y1dJYkhpWEJxdzJwVHBXbzdiOG05VWREbUMKSm9KaHhWVkJvbThoOTJtaC9iVXFSOHdaY2Jad1JZaXRFWGNqWkZzeElsZkFidytMOFlNMm1jOTJmM05Wc21xZgp2d2hvN2oxVUVzT2dnaUZJK29BRjhyZVVaT1FwYllGWDJxK2thWjlMTEhHakhyR3pndjlIV1pLL0tuMWRsdUhlCi9QYWJhbjdlZmZwbmE3RlMxUnFnWVM1S1FTL1g5ek1FZFhxMHdWY2VXK3plUEI4Q2dnRUJBTkNvY3RiSk90N04KcVB6V3JMY2lveFFQSDNXWVc0MGMzbEE0bjdhSUw2QlRZaUhNMkM0c1lrNlYyR2hDOXc5Z1hHd0tIdlZFZm92TQozOXhCVGR6aFNVS1FWZGlidDcrWEdLcTlwejBmTmFHb2YwOUdJQjVuMDNkeWxDd3hGdURLbVFzKzRyUDFZaEEwCnA0bldGclNuWU1tamlpRDhSQzVvS2NKbDRaV2tsdW91OWVJSkxpSTFHQm94eXpKS09jRy9WVjhvU0VjM0MwcHoKNXNsTC9UdFcwVjhyWHc0TkdpSCtRdkptek1TTUhQNzM0TG1GQ2VBQ01vbzZ1Ymk2VVpyTUx5YTlLRXVoTmtwZApEVm4vOWZKVkFHbWsyNmNSc1pNRWRTMjBaOGVLY2dpd2o5YndaY2k1ZTZOb3B6RHRobHQySlZ6aW13S0E0bU5SCmcrMlM0SDdyS2drQ2dnRUJBTXU4bTEra0Rjc0k0VFo3dk90d0FVS0pMdkM0Zmd5ZklHcVVVTkxBVzdvcWJMZ04KTHpYUkdPM0d6aDkxcEgxU2hJVU9FaEpjYVNla0lzM3pkRy84UXBORUlHSGI0UWlsZUF6OXQxL0NZM0Z1NDBDVwpoVjFuL0lUejA1QUFWSU81STlwSzhPdy96VGhjQUtlY0ZldHNvbmwycTBCM1lyTlIvbjJnT2wwUVhNNzZWSjlnCnFxeDY3S2lUTUREYWM4bU91NmZNZjBXWE44OWgrdjJ1NGZxNGwzd0JEYjVCdlRwVWwrOVd6ODY1dHZ4aGdoQmYKSmoxQXdGdmR0WENKNTZxUGFURFBjUW5VaFI5aStFZXVFVStvZkhwYzY0ZUJGRHBLQ3BBNnBvWG1lUDlkaTNvegpPa1cwNytobnYzdEZQb0ZXSnd1UmlSdDdPVGVzZW4rN0VVRzFxeDhDZ2dFQkFNUklVWTgvU1FQaWp6OFphdXZ6Cngxak5TUmpFTkFzd3h1akwrMklvQmpWWmV3eTB0Ly9uZktyVVJ2ajlWWSsvWS9HMks1MHB5NmtURnRzSkM0Zm0KM1pXaVZUOG9iTmZyK1NaVXRnZkhYaG91ZVdSSFlWdDQ2L0tIZEYrRjVqR2NKQS95SGl5UGZxb2ovaDZwcUQ1VQozYldSQkJVM2VhUDJGS0pzV0tMK2E0RHNvQno3NmN3QUNpWktCUTNQeVRrN0ZjSW9FS1g5WDcrdis4QU1uYkMrCllNS1grQmt0dGhHWUw3MzdlbERDUlZ5TUxYZ1hjRXlyaElPSDdzR1pjNzZjcVJaV0JwdnVrVFd2QnlwTW9ENVEKTzgwbGFnTmxuNWZmTDZQSSszODMrMEcxWmZvWnRvWDJCaHFZRGY3MjBkY2o4czNIMVcycmRFdGRtUzlaWlptKworcEVDZ2dFQUdRSFh3eVFTakpJRi85cy9uSVFxK0NrRGdqUXNCV0g3Y0hRRzJpbXIvc1hLYU5qdE80YWJNYSs0ClNnR1ZiZlViSlF6S0tQa2hHVG40QVh1ZzUzbzRUd1R2TlFqUDFtK3ZVSE0ySEV2ZmRaYnNhVmpoVUo3OE9JVE4KOTNaa3czR0VTOXRQTlY3U2tDUGQwcHBmR0plTkNtZkJiaWcvSVhwdi9PclFubThtOUM2K0pVYjhvTnl4T3IrTwpndHNrRWZvVTRiMW15cjdDczdMZnBSb3gwRmhoamc1NDlVOW56TWE3eCtDczhwcmNvUUZzOU1Wc1JwZkU5cGhHCkljQWx4SnFlakVYUk1Pakd3b1RtWGV4K3pVK3dlR2F3K0kzNXpDR1lRbURyeC80WmxmK3VKNmg0ZXB1YzdjaGIKYXkxNVVjaWZIY0VPeVBUQUtSbnBlSVJSZ2xYWUdBPT0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=", + "Store": "default" + } + ] + } +} \ No newline at end of file diff --git a/test/traefik_v3.bats b/test/traefik_v3.bats new file mode 100644 index 00000000..cd2f7e1a --- /dev/null +++ b/test/traefik_v3.bats @@ -0,0 +1,79 @@ +load 'test_helper/bats-support/load' +load 'test_helper/bats-assert/load' + +# +# traefik v3 +# +# acme.json as written by Traefik 2 and 3, which share the same format. The +# fixture holds two certificate resolvers: with more than one of them, the +# extraction used to fail and no certificate was written at all. +# + +@test "checking traefik v3: acme.json exist" { + run docker exec mailserver_traefik_v3 [ -f /etc/letsencrypt/acme/acme.json ] + assert_success +} + +@test "checking traefik v3: acme.json has more than one certificate resolver" { + run docker exec mailserver_traefik_v3 /bin/sh -c "jq -r 'keys | length' /etc/letsencrypt/acme/acme.json" + assert_success + [ "$output" -gt 1 ] +} + +# fixtures_traefik_v3 rewrites acme.json once. A watcher that reacted to +# its own reads would start the cycle again and again. The rewritten bytes +# are identical, so the cycle ends at "Live Certificates match". +@test "checking traefik v3: one write to acme.json causes exactly one reload" { + run docker logs mailserver_traefik_v3 + assert_success + [ "$(echo "$output" | grep -c 'Updating SSL certificates and reloading')" -eq 1 ] + [ "$(echo "$output" | grep -c 'Live Certificates match')" -eq 1 ] +} + +@test "checking traefik v3: the dump log was removed" { + run docker exec mailserver_traefik_v3 [ -f /var/mail/ssl/acme_dump.log ] + assert_failure +} + +@test "checking traefik v3: all certificates were generated" { + run docker exec mailserver_traefik_v3 [ -f /ssl/cert.pem ] + assert_success + run docker exec mailserver_traefik_v3 [ -f /ssl/chain.pem ] + assert_success + run docker exec mailserver_traefik_v3 [ -f /ssl/fullchain.pem ] + assert_success + run docker exec mailserver_traefik_v3 [ -f /ssl/privkey.pem ] + assert_success +} + +@test "checking traefik v3: check private key" { + run docker exec mailserver_traefik_v3 /bin/sh -c "openssl rsa -in /ssl/privkey.pem -check 2>/dev/null | head -n 1" + assert_success + assert_output "RSA key ok" +} + +@test "checking traefik v3: private key matches the certificate" { + run docker exec mailserver_traefik_v3 /bin/sh -c "(openssl x509 -noout -modulus -in /ssl/cert.pem | openssl md5 ; openssl rsa -noout -modulus -in /ssl/privkey.pem | openssl md5) | uniq | wc -l" + assert_success + assert_output 1 +} + +# +# ssl +# + +@test "checking ssl: the certificate from acme.json is the one served" { + run docker exec mailserver_traefik_v3 /bin/sh -c "timeout 1 openssl s_client -ign_eof -connect 0.0.0.0:587 -starttls smtp 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null" + assert_success + assert_output --partial "DNS:mail.domain.tld" +} + +# +# logs +# + +@test "checking logs: /var/log/mail.err in mailserver_traefik_v3 does not exist" { + run docker exec mailserver_traefik_v3 cat /var/log/mail.err + assert_failure + assert_output --partial 'No such file or directory' +}