Minimal Traefik v2 Certificate Export

After migrating from Traefik v1 to v2, I also had to re-adjust my certificate export script. The export script enabled me to re-use the certificates obtained from Let’s Encrypt in other (not web-related) services.

A web search revealed several tools that would already accomplish this task, but most of them were way too complex for my usecase (I don’t need the kitchen-sink, just exported certificates!). Nevertheless, you might find them useful:

Also, lots of the tools online (usually those that don’t explicitly mention “v2”) are written for Traefik v1, but the ACME storage formats for Traefik v1 and Traefik v2 are slightly different: “let’s break stuff because we’re awesome” – Traefik developers.

In any case, based on the tools mentioned above and the new format, I adjusted my old script and extended it with the following features:

  • only update certificates if necessary (you can attach an inotify handler to the files to listen for updates)
  • safe (more checks and error handling)
  • secure (i.e. an attacker cannot read secret contents through race conditions)

The script only requires a POSIX-compatible shell and the jq command-line utility.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/sh
# ./export-traefik-v2-certificate.sh DOMAIN

set -e # abort on errors
set -u # abort on unset variables

# adjust these variables according to your setup
TRAEFIK_CERT_STORE="/etc/traefik/acmev2.json"
TRAEFIK_RESOLVER="le"
OUTPUT_DIR=/etc/ssl/private/

DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
    echo "No domain given"
    exit 1
else
    KEY_FILE="${OUTPUT_DIR}/${DOMAIN}.key"
    NEW_KEY_FILE="$(mktemp /tmp/XXXXX.key.new)"
    CERT_FILE="${OUTPUT_DIR}/${DOMAIN}.crt"
    NEW_CERT_FILE="$(mktemp /tmp/XXXXX.crt.new)"
fi

# minor sanity checks
if [ ! -r "$TRAEFIK_CERT_STORE" ]; then
    echo "File $TRAEFIK_CERT_STORE not readable!"
    exit 1
fi
if ! grep "\"${DOMAIN}\"" "$TRAEFIK_CERT_STORE" > /dev/null; then
    echo "Domain $DOMAIN not found in $TRAEFIK_CERT_STORE"
    exit 1
fi

# create new files with strict permissions
touch "$NEW_KEY_FILE" "$NEW_CERT_FILE"
chown root:ssl-cert "$NEW_CERT_FILE" "$NEW_KEY_FILE"
chmod 640 "$NEW_CERT_FILE" "$NEW_KEY_FILE"

# extract certificate
cat "$TRAEFIK_CERT_STORE" | jq -r ".${TRAEFIK_RESOLVER}.Certificates[] | select(.domain.main==\"${DOMAIN}\") | .certificate" | base64 -d > "$NEW_CERT_FILE"

# extract private key
cat "$TRAEFIK_CERT_STORE" | jq -r ".${TRAEFIK_RESOLVER}.Certificates[] | select(.domain.main==\"${DOMAIN}\") | .key" | base64 -d > "$NEW_KEY_FILE"

# check if the contents changed
if ! diff -N "$NEW_CERT_FILE" "$CERT_FILE" > /dev/null; then
    # certificate changed, rotate files
    echo "Certificate $DOMAIN updated"
    mv "$NEW_CERT_FILE" "$CERT_FILE"
    mv "$NEW_KEY_FILE" "$KEY_FILE"
else
    # certificate unchanged, delete temporary files
    echo "Certificate $DOMAIN unchanged"
    rm -f "$NEW_CERT_FILE" "$NEW_KEY_FILE"
fi

exit 0

At the top of the script, you need to specify three variables: the path to Traefik’s ACME storage file, the output directory and the name of the certifcate resolver. I’m using “le” as the name, but depending on your Traefik configuration it might be called “letsencrypt” or similar.

When you call the script with ./export-traefik-v2-certificate.sh example.com, it will create a file called example.com.crt (containing the certificate chain) and another file called example.com.key (containing the private key) in the output directory.

The script does this by first creating new files (with random file names) and assigning strict permissions to them. Then, it exports the certificate and private key into those files. Afterwards, it compares these new files (certificate and private key) with the old files. If the contents differ, the old files will be replaced with the new ones. If not, the new ones will simply be deleted.

This design enables you to attach a listener (such as an inotify handler) to the original files and run appropriate actions (service restart, reload etc.) when necessary.

One aspect that this script doesn’t cover is handling of wildcard ACME certificates (or at least I didn’t test it).

Feel free to use and adapt this script to your liking. If you come across any errors or you have suggestions, please let me know!