Traefik is the reverse proxy of choice for many container and Kubernetes environments, partly because of its automatic Let's Encrypt integration. Define a router with TLS, add the right labels or annotations, and Traefik handles certificate issuance and renewal without further intervention. It's one of the genuinely good pieces of automation in the cloud-native ecosystem.
The problem is what happens when it fails. Traefik's ACME renewal process can encounter errors silently — rate limit hits, DNS propagation issues, challenge failures, ACME server unreachability — and the only sign is an approaching expiry date that nobody checked. This article covers how to monitor Traefik SSL certificate status before a failure becomes an outage.
Traefik stores ACME-issued certificates in a JSON file called acme.json (or in a Kubernetes Secret when using the Kubernetes provider). The file contains the certificate, private key, and renewal metadata for every domain Traefik manages:
# Default location for Docker/bare-metal deployments
cat /letsencrypt/acme.json | python3 -m json.tool | grep -A5 '"domain"'
Traefik renews certificates automatically when they have fewer than 30 days remaining. The renewal process uses either HTTP-01 or DNS-01 ACME challenges, depending on your configuration.
Key insight: Traefik continues serving the existing certificate while renewal is attempted. If renewal fails, Traefik keeps serving the old cert without error — right up until it expires and browsers start showing warnings to your users.
If you have the Traefik dashboard enabled, navigate to HTTP → Routers and inspect TLS-enabled routers. The dashboard shows which ACME resolver each router uses, but it does not prominently display certificate expiry dates.
For expiry details, the API endpoint is more useful:
# Traefik API — list all ACME certificates
curl -s http://localhost:8080/api/overview | python3 -m json.tool
# Or query the router detail for TLS info
curl -s http://localhost:8080/api/http/routers | python3 -m json.tool | grep -A10 "tls"
Traefik's acme.json contains the base64-encoded certificate for each domain. Parse it to extract expiry dates:
#!/bin/bash
# check-traefik-certs.sh — parse acme.json and print expiry for each domain
ACME_FILE="/letsencrypt/acme.json"
python3 - <
This script iterates over all resolvers in acme.json and prints days remaining for each domain — sorted by resolver, with WARN for anything under 30 days.
Parsing acme.json tells you what Traefik has stored. Checking the live endpoint tells you what it's actually serving — which differs when Traefik has loaded a stale cert into memory or when a CDN sits in front:
# Check what Traefik is actually serving to the world
echo | openssl s_client -connect yourdomain.com:443 -servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -enddate -issuer
# Days remaining
expiry=$(echo | openssl s_client -connect yourdomain.com:443 \
-servername yourdomain.com 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo $(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 )) days remaining
Let's Encrypt imposes rate limits: 50 certificates per registered domain per week, 5 duplicate certificates per week, and 5 failed validations per hostname per hour. If you redeploy frequently, reconfigure routers often, or run many subdomains, you can hit these limits. When a rate limit is hit, Traefik logs an error but continues serving the expiring cert:
# Check Traefik logs for rate limit errors
docker logs traefik 2>&1 | grep -i "rate\|too many\|urn:ietf:params:acme:error"
# For systemd deployments
journalctl -u traefik --since "7 days ago" | grep -i "acme\|renew\|error"
Traefik handles HTTP-01 challenges by temporarily serving a validation token on port 80. This fails when:
/.well-known/acme-challenge/ path# Test that the ACME challenge path is reachable
curl -I http://yourdomain.com/.well-known/acme-challenge/test-token
For wildcard certs or environments where HTTP-01 is impractical, Traefik supports DNS-01 challenges via provider APIs. These fail when:
# Check for DNS challenge errors in Traefik logs
docker logs traefik 2>&1 | grep -i "dns\|challenge\|propagation"
Traefik requires acme.json to have permissions of 600. If the file permissions change (common after volume remounts or container restarts with different UID mappings), Traefik may refuse to write renewal updates:
ls -la /letsencrypt/acme.json
# Should show: -rw------- (600)
chmod 600 /letsencrypt/acme.json
If acme.json is stored inside the container rather than in a mounted volume, every container restart means Traefik must re-issue all certificates from scratch. In a clean environment this works. During a Let's Encrypt rate limit window, it means all domains go without valid certs until the window expires:
# Verify the acme.json volume mount in your compose config
docker inspect traefik | python3 -m json.tool | grep -A5 "Mounts"
When using Traefik in Kubernetes (via Helm or the official operator), certificates are stored in Kubernetes Secrets rather than acme.json. Check them with:
# List TLS secrets used by Traefik IngressRoute resources
kubectl get ingressroute --all-namespaces -o json \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for item in data['items']:
for route in item.get('spec', {}).get('tls', {}).get('secretName', []):
ns = item['metadata']['namespace']
print(f'{ns}/{route}')
"
# Check expiry of a specific TLS secret
kubectl get secret traefik-tls -n default \
-o jsonpath='{.data.tls\.crt}' | base64 -d \
| openssl x509 -noout -enddate
Traefik's built-in automation is genuinely useful, but a complete monitoring setup adds an independent external check:
The goal is a second opinion on what's actually being served — one that runs whether Traefik is logging errors or not.
Enter any domain Traefik manages and see days remaining, expiry date, and issuer instantly. No Traefik access or acme.json parsing required.
Check SSL expiry → Get expiry alerts →SSLWatch monitors SSL certificate expiry for any HTTPS endpoint — including all domains your Traefik instance manages — with external TLS checks and email alerts at 30, 14, 7, and 1 day before expiry. It works independently of Traefik's renewal process, so you get advance warning even when Traefik's ACME integration fails silently.
SSLWatch monitors your domains with external TLS checks independent of Traefik's renewal state — alerts arrive before the cert becomes a problem.
Join the SSLWatch waitlist → Free expiry checker →