@@ -24,6 +24,20 @@ SUBDOMAINS="${SUBDOMAINS:?Must set SUBDOMAINS (space-separated list)}"
2424ECH_ROTATION=" ${ECH_ROTATION:- false} " # default: disabled
2525KEEP_KEYS=" ${KEEP_KEYS:- 3} " # number of old timestamped keys to keep
2626
27+ reload_nginx () {
28+ if [[ -f " $PIDFILE " ]]; then
29+ PID=$( cat " $PIDFILE " )
30+ if kill -0 " $PID " 2> /dev/null; then
31+ kill -SIGHUP " $PID "
32+ log " Reloaded nginx (pid $PID )"
33+ else
34+ log " Nginx PID $PID is not yet running"
35+ fi
36+ else
37+ log " PID file not found: $PIDFILE "
38+ fi
39+ }
40+
2741rotate_ech () {
2842 log " Rotating ECH keys..."
2943 mkdir -p " $ECH_DIR " || log " Failed to create $ECH_DIR "
@@ -35,27 +49,88 @@ rotate_ech() {
3549
3650 # 2. Ensure symlinks exist, fill missing ones with latest
3751 cd " $ECH_DIR " || return 1
52+
53+ # Before rotation, capture current symlinks for rollback
54+ old_latest=$( readlink -f " $DOMAIN .ech" 2> /dev/null || true)
55+ old_previous=$( readlink -f " $DOMAIN .previous.ech" 2> /dev/null || true)
56+ old_stale=$( readlink -f " $DOMAIN .stale.ech" 2> /dev/null || true)
57+
3858 ln -sf " $( readlink " $DOMAIN .previous.ech" ) " " $DOMAIN .stale.ech"
3959 ln -sf " $( readlink " $DOMAIN .ech" ) " " $DOMAIN .previous.ech"
4060 ln -sf " $( basename " $NEW_KEY " ) " " $DOMAIN .ech"
4161 log " Symlinks rotated: ech -> $( readlink " $DOMAIN .ech" ) , previous.ech -> $( readlink " $DOMAIN .previous.ech" ) , stale.ech -> $( readlink " $DOMAIN .stale.ech" ) "
4262
4363 # 4. Reload nginx
44- if [[ -f " $PIDFILE " ]]; then
45- PID=$( cat " $PIDFILE " )
46- if kill -0 " $PID " 2> /dev/null; then
47- kill -SIGHUP " $PID "
48- log " Reloaded nginx (pid $PID )"
49- else
50- log " Nginx PID $PID is not yet running"
51- fi
52- else
53- log " PID file not found: $PIDFILE "
54- fi
64+ reload_nginx
65+
66+ # 5. Backup DNS records for rollback
67+ backup_file=$( mktemp)
68+ curl -s -X GET " $CF_ZONE_URL /$CF_ZONE_ID /dns_records?type=HTTPS" \
69+ -H " Authorization: Bearer $CF_API_TOKEN " \
70+ -H " Content-Type: application/json" \
71+ > " $backup_file "
5572
5673 # 5-6. Update DNS Records
5774 source /usr/local/bin/update_https_records.sh
58- update_https_records || { log " Error: Failed to update HTTPS DNS records" ; return 1; }
75+ # DNS update
76+ if ! update_https_records; then
77+ log " Error: Failed to update HTTPS DNS records, rolling back ECH keys in nginx..."
78+
79+ # Roll back symlinks to old state
80+ [[ -n " $old_latest " ]] && ln -sf " $( basename " $old_latest " ) " " $DOMAIN .ech"
81+ [[ -n " $old_previous " ]] && ln -sf " $( basename " $old_previous " ) " " $DOMAIN .previous.ech"
82+ [[ -n " $old_stale " ]] && ln -sf " $( basename " $old_stale " ) " " $DOMAIN .stale.ech"
83+
84+ # Optionally delete the new key if not needed
85+ rm -f -- " $NEW_KEY "
86+ log " Deleted the newly generated key: ${NEW_KEY} "
87+ reload_nginx
88+
89+ log " Rolling back DNS updates"
90+ # Get current state
91+ current_file=$( mktemp)
92+ curl -s -X GET " $CF_ZONE_URL /$CF_ZONE_ID /dns_records?type=HTTPS" \
93+ -H " Authorization: Bearer $CF_API_TOKEN " \
94+ -H " Content-Type: application/json" \
95+ > " $current_file "
96+
97+ # Collect rollback candidates
98+ ROLLBACK=()
99+ while IFS= read -r rec; do
100+ rec_id=$( jq -r ' .id' <<< " $rec" )
101+ cur=$( jq -c --arg id " $rec_id " ' .result[] | select(.id==$id)' " $current_file " )
102+
103+ if [[ " $rec " != " $cur " ]]; then
104+ log " Will restore record $rec_id "
105+ # Make sure to keep only fields CF accepts, including id
106+ clean=$( jq ' {id, type, name, content, ttl, proxied, priority, data, comment, tags}' <<< " $rec" )
107+ ROLLBACK+=(" $clean " )
108+ fi
109+ done < <( jq -c ' .result[]' " $backup_file " )
110+
111+ if [ " ${# ROLLBACK[@]} " -gt 0 ]; then
112+ # Build batch body with puts
113+ PUTS_JSON=$( printf ' %s\n' " ${ROLLBACK[@]} " | jq -s ' .' )
114+ BATCH=$( jq -n --argjson puts " $PUTS_JSON " ' {puts:$puts}' )
115+
116+ log " Submitting rollback batch with ${# ROLLBACK[@]} records: $BATCH "
117+ CF_RESULT=$( curl -s -X POST " $CF_ZONE_URL /$CF_ZONE_ID /dns_records/batch" \
118+ -H " Authorization: Bearer $CF_API_TOKEN " \
119+ -H " Content-Type: application/json" \
120+ --data " $BATCH " )
121+
122+ if echo " $CF_RESULT " | grep -q ' "success":true' ; then
123+ log " Rollback batch applied successfully"
124+ else
125+ log " Rollback batch failed: $CF_RESULT "
126+ fi
127+ else
128+ log " No changes detected, nothing to rollback"
129+ fi
130+
131+ log " ECH key rotation failed, rollback successful"
132+ return 1
133+ fi
59134
60135 # 7. Cleanup old keys (keep latest N timestamped files, skip symlink targets)
61136 cd " $ECH_DIR " || return 1
0 commit comments