Jeffrey Harris

Remove TLS domains from Traefik

When using Traefik as an application proxy, there is a minor shortcoming, and that is the fact that it uses the acme.json file to renew the domain names instead of using active HTTP Routers. Advantages and disadvantages to either one. I'm not in charge of that, so I can't change it, but what I can do is work around it.

I have some domains that I didn't renew, and domains that I have graduated to GitLab Pages, but traefik keeps trying to renew the domain. The way to make Traefik know to not renew the certificate is to remove it from the acme.json file.

Sure, you could use nano or your favorite text editor to modify acme.json, but the lines can get long, and it's easy to make errors. Luckly, there is jq to the rescue. So let's wrap it inside a nice BASH script so we don't break files and it's easy to remove domains that we don't want anymore.

#!/bin/bash
# remove-domain: Remove a domain from Traefik's acme.json
#
# Usage: ./remove-domain <domain-to-remove> [resolver]
#   - <domain-to-remove> : domain you want to remove from acme.json
#   - [resolver]         : top-level key in acme.json (defaults to 'lets-encrypt')
#                          This should match a key in your traefik.yml
# Example:
#   ./remove-domain www.example.com
#   ./remove-domain www.example.com valentinesresolver

set -e

# === Sanity check: Ensure jq is installed ===
JQ_CMD=$(command -v jq || true)
if [ -z "$JQ_CMD" ]; then
  echo "Error: jq is not installed or not in your PATH. Aborting."
  exit 1
fi

# === User-configurable variables ===

# Directory containing acme.json (must end with /)
ACME_DIR="/path/to/traefik/"
ACME_FILE_NAME="acme.json"
RESOLVER="${2:-lets-encrypt}"  # Default resolver; should match traefik.yml certificatesResolvers key

# certificatesResolvers:
#  lets-encrypt: <-- *this* should match $RESOLVER, or pass it as the second parameter.

# === End of user-configurable variables ===

# Check input
if [ -z "$1" ]; then
  echo "Usage: $0 <domain-to-remove> [resolver]"
  exit 1
fi

DOMAIN="$1"

TIMESTAMP=$(date +%Y%m%d%H%M)
BACKUP_FILE="${ACME_DIR}acme-${TIMESTAMP}.json"
ACME_FILE="${ACME_DIR}${ACME_FILE_NAME}"

# Backup acme.json
cp "$ACME_FILE" "$BACKUP_FILE"
echo "Backup created: $BACKUP_FILE"

# Remove domain from specified resolver
jq --arg domain "$DOMAIN" --arg resolver "$RESOLVER" '
  del(
    .[$resolver].Certificates[]
    | select(
        (.domain.main == $domain)
        or (.domain.sans // [] | index($domain))
      )
  )
' "$ACME_FILE" > "${ACME_FILE}.tmp"

# Replace acme.json with updated version and fix permissions
mv "${ACME_FILE}.tmp" "$ACME_FILE"
chmod 600 "$ACME_FILE"

echo "Domain '$DOMAIN' removed from $ACME_FILE under resolver '$RESOLVER'"
echo "You should restart Traefik because $ACME_FILE is not hot-reloadable."

Just a couple of caveats, though. This is not atomic; if you run this command when Traefik is creating or renewing a certificate, it might break. But it make a backup, so you're not going to be entirely hosed. Also, the acme.json file is not hot-reloadable; restart Traefik after modifing the acme.json file.