Skip to content

Instantly share code, notes, and snippets.

@Reiner030
Last active September 15, 2025 09:12
Show Gist options
  • Select an option

  • Save Reiner030/8583a101148636e7368afc1c428593cb to your computer and use it in GitHub Desktop.

Select an option

Save Reiner030/8583a101148636e7368afc1c428593cb to your computer and use it in GitHub Desktop.
OPNSense ACME plugin tool to reorder within /conf/config.xml all (or by UUID defined) certificate(s) with their action tasks + backup
#!/bin/sh
# POSIX /bin/sh – OPNsense kompatibel
# Sortiert <restartActions> stabil: Copy-* → Sent-* → Reload-* → Restart-*
# und sortiert JEDE Gruppe alphabetisch nach Action-Name.
# Usage:
# ./sort_acme_restart_actions_posix.sh # anwenden
# ./sort_acme_restart_actions_posix.sh -n # Dry-Run
# CERT_UUID=<uuid> ./sort_acme_restart_actions_posix.sh # nur dieses Zertifikat
# vorsichtig sein, wenn gerade acme.sh läuft
pgrep -f "[a]cme.sh" >/dev/null 2>&1 && exit 0
set -eu
CFG="/conf/config.xml"
TS="$(date +%Y%m%d-%H%M%S)"
BAK="/conf/config.xml.bak.$TS"
TMP="$(mktemp /tmp/config.xml.XXXXXX)"
# Optional: -n = Dry-Run (nur anzeigen, nichts schreiben)
DRYRUN=0
[ "${1:-}" = "-n" ] && DRYRUN=1
# Nur dieses Zertifikat anfassen? (UUID)
TARGET_CERT="${CERT_UUID:-}"
# Tools prüfen
command -v xmllint >/dev/null 2>&1 || {
echo "xmllint nicht gefunden. Bitte ggf. 'pkg install libxml2'." >&2
exit 1
}
# Action-Name aus UUID ermitteln (leer => unbekannt)
get_action_name() {
uuid="$1"
xmllint --xpath "normalize-space(//AcmeClient/actions/action[@uuid='$uuid']/name)" "$CFG" 2>/dev/null || true
}
# CSV-Gruppe nach Name sortieren (alphabetisch, case-insensitive)
# Input: CSV; Output: CSV
sort_group() {
csv="$1"
[ -z "$csv" ] && { echo ""; return; }
tmpfile="$(mktemp /tmp/acmegrp.XXXXXX)"
OLDIFS="$IFS"; IFS=,
for u in $csv; do
u="$(echo "$u" | tr -d '[:space:]')"
[ -z "$u" ] && continue
n="$(get_action_name "$u")"
# sort key: name|uuid
printf '%s|%s\n' "${n:-zzz_unknown_$u}" "$u" >> "$tmpfile"
done
IFS="$OLDIFS"
# sortiere nach Name (case-insensitive), stabil
# und gebe nur die UUIDs als CSV aus
# shellcheck disable=SC2002
sorted="$(cat "$tmpfile" | sort -f | awk -F'|' 'BEGIN{first=1}{if(!first){printf(",")} printf $2; first=0}')"
rm -f "$tmpfile"
echo "$sorted"
}
# Zertifikats-UUIDs
cer_uuids="$(xmllint --xpath "//AcmeClient/certificates/certificate/@uuid" "$CFG" \
| sed 's/uuid="/\n/g' | sed 's/"//g' | awk NF)"
[ -z "$cer_uuids" ] && { echo "Keine Zertifikate gefunden."; exit 0; }
changed=0
for cuuid in $cer_uuids; do
[ -n "$TARGET_CERT" ] && [ "$cuuid" != "$TARGET_CERT" ] && continue
csv="$(xmllint --xpath "string(//AcmeClient/certificates/certificate[@uuid='$cuuid']/restartActions)" "$CFG" 2>/dev/null || true)"
[ -z "$csv" ] && continue
copies=""; sents=""; reloads=""; restarts=""
OLDIFS="$IFS"; IFS=,
for u in $csv; do
u="$(echo "$u" | tr -d '[:space:]')"
[ -z "$u" ] && continue
name="$(get_action_name "$u")"
case "$name" in
Copy-*) copies="${copies:+$copies,}$u" ;;
Sent-*) sents="${sents:+$sents,}$u" ;;
Restart-*) restarts="${restarts:+$restarts,}$u" ;;
*) reloads="${reloads:+$reloads,}$u" ;;
esac
done
IFS="$OLDIFS"
# JEDE Gruppe sortieren (nach Name), dann in definierter Reihenfolge zusammenführen
copies_sorted="$(sort_group "$copies")"
sents_sorted="$(sort_group "$sents")"
reloads_sorted="$(sort_group "$reloads")"
restarts_sorted="$(sort_group "$restarts")"
newcsv="$copies_sorted"
[ -n "$sents_sorted" ] && newcsv="${newcsv:+$newcsv,}$sents_sorted"
[ -n "$reloads_sorted" ] && newcsv="${newcsv:+$newcsv,}$reloads_sorted"
[ -n "$restarts_sorted" ] && newcsv="${newcsv:+$newcsv,}$restarts_sorted"
if [ "$newcsv" != "$csv" ]; then
echo "Zertifikat $cuuid: Reihenfolge wird angepasst:"
echo " ALT: $csv"
echo " NEU: $newcsv"
if [ $DRYRUN -eq 0 ]; then
if [ $changed -eq 0 ]; then
cp "$CFG" "$BAK"
echo "Backup: $BAK"
fi
awk -v uuid="$cuuid" -v newcsv="$newcsv" '
/<certificate[[:space:]]+uuid="/ {
if ($0 ~ "<certificate[[:space:]]+uuid=\"" uuid "\"") in_cert=1; else in_cert=0
}
in_cert && match($0, /<restartActions>.*<\/restartActions>/) {
sub(/<restartActions>.*<\/restartActions>/, "<restartActions>" newcsv "</restartActions>")
}
{ print }
in_cert && /<\/certificate>/ { in_cert=0 }
' "$CFG" > "$TMP" && mv "$TMP" "$CFG"
changed=1
fi
fi
done
if [ $DRYRUN -eq 1 ]; then
echo "Dry-Run beendet. Nichts geschrieben."
exit 0
fi
if [ $changed -eq 1 ]; then
echo "Konfiguration aktualisiert. Lade Config neu…"
configctl config reload >/dev/null 2>&1 || true
echo "Fertig. GUI kann weiter 'gemischt' aussehen; die Abarbeitung folgt jetzt: Copy → Sent → Reload → Restart (mit interner Sortierung)."
else
echo "Keine Änderung erforderlich."
fi
@Reiner030
Copy link
Author

Reiner030 commented Sep 15, 2025

OPNsense ACME Client – Automation Order Helper Scripts

These helper scripts address a bug in the OPNsense ACME client plugin (os-acme-client 4.9 on OPNsense Business 25.4.3) where the GUI does not preserve the order of automations.
On save/re-open, the <restartActions> list in /conf/config.xml may be reordered (often UUID-driven), causing Reload-* actions to run before Copy-/Sent-*.
That can lead to services reloading with old certificates still in place.

The issue is tracked here: opnsense/plugins#4940


Scripts

Each run makes a backup of /conf/config.xml and reloads configd.


Usage

Show current order

/root/acme_show_certificate_orders.sh

Example output:

Certificate d7a1cb5e-dbb6-43f6-b951-908b65d9f54f:
  8ad82a0a-d59f-45e1-801e-451ade9698df  (Copy-Certificate-to-intranet01)
  dd96ff98-b9d3-4a7b-9580-ef6c0b41b5ea  (Reload-Certificate-on-intranet01)
  880b5435-08b2-48cc-bd85-28ddd6e54a6e  (Restart-OPNsense-WebGUI)

Sort order

Dry run (no changes):

/root/sort_acme_restart_actions_posix.sh -n

Apply (writes backup + new order):

/root/sort_acme_restart_actions_posix.sh

Target a single certificate only:

CERT_UUID=<uuid> /root/sort_acme_restart_actions_posix.sh

Automate with cron

Run regularly to correct after GUI edits:

0 * * * * root /root/sort_acme_restart_actions_posix.sh

The script automatically aborts if acme.sh is currently running.


Example Diff

Before:

<restartActions>Reload-...,Copy-...,Restart-...</restartActions>

After:

<restartActions>Copy-...,Sent-...,Reload-...,Restart-...</restartActions>

Notes

  • The GUI may still display a mixed order on edit.
  • Execution order follows the CSV in config.xml, which this script fixes.
  • Both scripts are standalone POSIX sh, no bash required, tested on OPNsense Business 25.4.3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment