ใบ wildcard ของ DA หยุดต่ออายุเงียบ ๆ หลังย้าย NS ไป Cloudflare
ใบรับรองที่ต่ออายุไม่ได้ไม่เคยส่งเสียงเตือนก่อนหมดอายุ มันแค่หยุดทำงานเงียบ ๆ ตั้งแต่วันที่มีคนย้าย NS แล้วรอให้ผู้ใช้เป็นคนเจอเอง ตัวอย่างนี้คือใบ wildcard ที่ผูกกับ DNS ของเครื่องเราโดยที่ไม่มีใครนึกถึง
สรุปใน 1 นาที
- อาการ/ปัญหา
- ใบ wildcard ต่ออายุไม่ได้แบบเงียบ ๆ หลังลูกค้าย้าย NS ไป Cloudflare จนใบหมดอายุ
- ใครควรอ่าน
- คนดูแล DirectAdmin ที่ออกใบ wildcard ด้วย DNS-01
- ทางแก้แบบสั้น
- สแกนหาโดเมนที่ย้าย NS ไปแล้ว ออกใบใหม่แบบระบุชื่อ และตรวจพอร์ตจริงหลัง reload ว่าได้ใบใหม่
พอดีผมเจอใบ wildcard ที่หมดอายุไปแล้วเมื่อเดือนก่อนโดยบังเอิญ ตอนนั้นกำลังทำงานเรื่องความเร็วของอีกเว็บหนึ่ง เลยตรวจ TLS ของเว็บข้างเคียงเพิ่ม แล้วเห็น apex ของโดเมนลูกค้ารายหนึ่งส่งใบ *.example.com ที่ DirectAdmin ออกเองแต่ไม่ถูกต่ออายุ เบราว์เซอร์ขึ้นคำเตือนใบรับรอง โปรแกรมเมลของฝั่งนั้นก็บ่นเรื่อง TLS ด้วย และไม่มี alert อะไรเลยที่ทำให้ผมรู้ล่วงหน้า
ชื่อโดเมน user และ IP ในบทความนี้เป็นค่าตัวอย่างทั้งหมด (example.com, webuser, 203.0.113.10) วันที่ของเหตุการณ์ผมตัดออก
อาการ: ใบหมดอายุไปหลายสัปดาห์ ไม่มีใครเตือน
สิ่งที่เห็นมีสองอย่างในเรื่องเดียวกัน อย่างแรกคือเว็บ ใบที่ส่งออกไปหมดอายุแล้ว อย่างที่สองคือเมล พอร์ต IMAP POP และ SMTP ใช้ใบเดียวกัน ฝั่งผู้ใช้เลยเห็นเตือนทั้งสองทาง ใบนี้ไม่ใช่ใบที่ผมสร้างเอง เป็น Let's Encrypt wildcard ที่ DirectAdmin ออกให้เอง
ผมเริ่มด้วยการเช็กว่างานต่ออายุของ DirectAdmin ยังรันอยู่ไหม พอเปิด log ของ DirectAdmin ดู งานต่ออายุยังรันตามรอบ แต่ล้มแบบเงียบ ๆ ซึ่งแปลว่า cron ไม่ได้พัง ปัญหาอยู่ที่ตัวการพิสูจน์ความเป็นเจ้าของโดเมนที่ Let's Encrypt ขอ
ทำไมต่ออายุไม่ได้: wildcard ต้องใช้ DNS-01 และ DNS-01 ต้องพึ่ง NS ของเรา
ใบ wildcard ออกได้ด้วยวิธีพิสูจน์แบบ DNS-01 วิธีเดียว คือต้องวาง TXT record ชื่อ _acme-challenge ในโซนของโดเมนให้ Let's Encrypt มองเห็น DirectAdmin ทำเรื่องนี้โดยเขียน TXT ลง BIND ที่รันอยู่บนเครื่องเดียวกัน ซึ่งทำงานได้ตราบที่ NS ของโดเมนยังชี้มาที่เครื่องเรา
พอลูกค้าหรือเอเจนซีของเขาย้าย NS ไป Cloudflare โซนในเครื่องเราก็ไม่ใช่ผู้ตอบ DNS ของโดเมนนั้นอีกต่อไป งานต่ออายุยังรันตามเวลาเดิมและเขียน TXT ลง BIND ได้ตามปกติ แต่ Let's Encrypt ไปถามที่ Cloudflare ซึ่งไม่มี TXT นั้นอยู่ การพิสูจน์เลยล้ม และบันทึกไว้เฉพาะใน log ของ DirectAdmin ไม่มีอีเมลหรือหน้าจอไหนบอกใคร
ใบอายุประมาณ 90 วัน การต่ออายุเริ่มพยายามราววันที่ 60 เลยทำให้ช่วงที่ล้มเงียบนั้นยาวประมาณหนึ่งเดือนก่อนใบจะหมดจริง
พิสูจน์ผ่าน TXT ใน DNS
- ออก wildcard ได้
- ต้องคุม DNS ของโดเมนจริง
- ผูกกับว่า NS ชี้ไปที่ไหน
- ล้มแล้วไม่มีใครเห็น
พิสูจน์ผ่านไฟล์พอร์ต 80
- ออก wildcard ไม่ได้
- ต้องระบุชื่อทีละชื่อ
- ผูกกับว่าชื่อชี้มาที่เครื่องนี้
- ใช้ได้แม้ NS อยู่ที่อื่น
สองวิธีนี้ไม่มีวิธีไหนผิด ต่างกันที่เงื่อนไขซ่อนอยู่ DNS-01 มีเงื่อนไขว่าเราต้องเป็นผู้ตอบ DNS ของโดเมน ส่วน HTTP-01 มีเงื่อนไขว่าชื่อต้องชี้ตรงมาที่เครื่อง ซึ่งอย่างหลังเป็นเงื่อนไขที่ตรวจง่ายกว่ามากด้วย dig บรรทัดเดียว ถ้าชื่อไม่ชี้มาที่เรา ก็ไม่มีเหตุผลต้องถือใบของมันอยู่แล้ว

ช่วงที่ย้าย NS เว็บยังเปิดได้ ใบเดิมยังไม่หมดอายุ ไม่มีสัญญาณอะไรให้สงสัย ความเสียหายมาทีหลังเป็นเดือน ตอนที่คนย้าย NS ลืมไปแล้วว่าเคยย้าย และคนดูแลเครื่องไม่เคยได้ยินเรื่องนี้เลย
สแกนก่อน อ่านอย่างเดียว
ก่อนจะแก้อะไร ผมอยากรู้ว่าเสียไปกี่โดเมน เลยเขียนสคริปต์อ่านอย่างเดียววนไฟล์ใบของทุกโดเมนใน DirectAdmin แล้วดูวันที่เหลือ ตัวไหนเหลือน้อยกว่า 21 วันค่อยไปถามว่า A กับ NS ตอนนี้ชี้ไปไหน โดยถามที่ resolver ภายนอก ไม่ถามเครื่องตัวเอง เพราะ BIND ในเครื่องเรายังตอบโซนเก่าของโดเมนที่ย้ายไปแล้วได้
#!/bin/bash
# cert_scan.sh — อ่านอย่างเดียว ไม่แก้อะไร
for f in /usr/local/directadmin/data/users/*/domains/*.cert; do
d=$(basename "$f" .cert)
end=$(openssl x509 -enddate -noout -in "$f" | cut -d= -f2)
left=$(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 ))
[ "$left" -lt 21 ] || continue
a=$(dig +short A "$d" @1.1.1.1 | head -1)
ns=$(dig +short NS "$d" @1.1.1.1 | tr '\n' ' ')
echo "$left days $d A=${a:-none} NS=$ns"
done
ข้อสำคัญคืออ่านผลให้เป็น ตัวที่ใบใกล้หมดอายุไม่ได้แปลว่าต้องแก้ทุกตัว ต้องดูที่ A record ด้วยว่ายังชี้มาที่เครื่องเราหรือไม่
ต้องแก้ ถ้าใบใกล้หมด
- เว็บและเมลยังเสิร์ฟจากเครื่องนี้
- ผู้ใช้เห็นคำเตือนจริง
- ออกใหม่แบบระบุชื่อ
ปล่อยไว้ ไม่ต้องแตะ
- ใบเก่าในเครื่องเราเป็นของเหลือ
- ไม่มีใครใช้ ไม่มีผลอะไร
- แก้แล้วไม่ได้อะไรเพิ่ม
ผลสแกนรอบแรกเจอสองโดเมนที่เสียหายจริงและแก้แล้ว กับอีกหลายโดเมนที่ใบหมดอายุในเครื่องเราแต่ A ไม่ชี้มาที่เราแล้วเลย อันนั้นเป็นของเหลือเฉย ๆ ผมปล่อยไว้ ต่อมามีสคริปต์ตรวจใหญ่ขึ้นที่ต่อ TLS เข้าไปถามเครื่องจริงทีละชื่อด้วย openssl s_client -servername แล้วอ่านผู้ออกใบ วันหมดอายุ และชื่อตรงหรือไม่ ได้ผลเป็นสถานะ OK, NO-LE, EXPIRING และ SKIP (ไม่ได้เสิร์ฟจากเครื่องนี้)
check() { # host
info=$(echo | timeout 10 openssl s_client -connect 203.0.113.10:443 -servername "$1" 2>/dev/null \
| openssl x509 -noout -issuer -enddate -checkhost "$1" 2>/dev/null)
match=$(echo "$info" | grep -c 'does match')
end=$(echo "$info" | grep -oP 'notAfter=\K.*')
left=$(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 ))
if [ "$match" -eq 0 ]; then echo "NO-LE $1"
elif [ "$left" -lt 20 ]; then echo "EXPIRING $1 ($left days)"
else echo "OK $1"
fi
}
ทางแก้: ออกใหม่แบบระบุชื่อ ใช้ HTTP-01
สิ่งที่ผมทำคือเลิกใช้ wildcard กับโดเมนที่ย้าย NS แล้ว ออกใหม่เป็นใบที่ระบุชื่อทีละชื่อ (apex, www, mail, pop, smtp, ftp และ subdomain) ซึ่ง Let's Encrypt พิสูจน์ด้วยวิธี HTTP-01 คือวางไฟล์ใต้ /.well-known/acme-challenge/ แล้วเรียกผ่านพอร์ต 80 ไม่ต้องแตะ DNS เลย ใบแบบนี้ต่ออายุเองได้ต่อไปตราบที่ชื่อยังชี้มาที่เรา
ผมห่อขั้นตอนเป็น helper ตัวเดียว (บนเครื่องวางไว้ที่ /usr/local/sbin/le-reissue) ลำดับที่มันทำคือ สำรองใบ คีย์ conf และไฟล์ snidomains พร้อมไฟล์ RESTORE.md ก่อนเสมอ จากนั้นสร้างรายชื่อ SAN จากชื่อมาตรฐานของโดเมนกับ subdomain ที่ DirectAdmin มีอยู่ โดยเลือกเฉพาะชื่อที่ A record สาธารณะเท่ากับ IP ของเครื่องนี้ (ตัวอย่างข้างล่างย่อเหลือชื่อมาตรฐาน)
IP=203.0.113.10
NAMES=()
for h in "$D" "www.$D" "mail.$D" "pop.$D" "smtp.$D" "ftp.$D"; do
a=$(dig +short A "$h" @1.1.1.1 | grep -E '^[0-9.]+$' | head -1)
# ชื่อที่ชี้ไป Cloudflare (proxy) ข้ามไปก่อน เพราะ A ไม่ใช่ IP ของเรา
if [ "$a" = "$IP" ]; then NAMES+=("$h"); else echo " skip $h (A=${a:-none})"; fi
done
# apex ต้องอยู่ในรายการ ไม่งั้นหยุด ไม่ออกใบ
[ "${NAMES[0]}" = "$D" ] || { echo "[ERR] apex ไม่ได้ชี้มาที่เรา"; exit 3; }
พอได้รายชื่อก็เรียก DirectAdmin API ในนามของ user เจ้าของโดเมน เพื่อให้ DirectAdmin ออกใบและเขียนรายการ SNI ของเมลให้ตัวเอง ผมไม่ได้แก้ไฟล์ SNI ด้วยมือเลย ส่วน $URL ได้จาก directadmin api-url --user=webuser ซึ่งมี login key ติดมาด้วย เลยไม่เอามาลงในบทความ
ARGS=(-s --max-time 300 -k "$URL/CMD_API_SSL" -d action=save -d "domain=$D" -d type=create
-d request=letsencrypt -d "name=$D" -d keysize=secp384r1 -d encryption=sha256
-d background=no -d json=yes)
i=0; for n in "${NAMES[@]}"; do ARGS+=(-d "le_select$i=$n"); i=$((i+1)); done
curl "${ARGS[@]}"
ผมมีสวิตช์เสริมให้รวมชื่อเว็บที่อยู่หลัง Cloudflare proxy ด้วย (HTTP-01 ผ่าน proxy ได้) แต่ไม่รวมชื่อเมลเด็ดขาด เพราะเมลต้องชี้ตรงมาที่เครื่องเรา ไม่ผ่าน proxy
ตรวจจากพอร์ตจริง ไม่ใช่จากไฟล์
ข้อที่พลาดง่ายที่สุดของงานนี้คือเชื่อว่า "ไฟล์ใหม่แล้ว = ใช้ใบใหม่แล้ว" DirectAdmin เซฟไฟล์ใหม่ลงดิสก์ได้เลย แต่ nginx กับ dovecot ยังถือใบเก่าอยู่ในหน่วยความจำจนกว่าจะ reload และหลัง reload แบบ graceful process เก่ายังตอบด้วยใบเก่าอีกสองสามวินาที ผมเลยให้ helper reload แล้วตรวจจากพอร์ตจริง พร้อมลองซ้ำ
chk() { # port host
for t in 1 2 3 4; do
r=$(echo | timeout 15 openssl s_client -connect "$IP:$1" -servername "$2" -verify_hostname "$2" 2>/dev/null \
| grep -m1 'Verify return code')
echo "$r" | grep -q ' 0 (ok)' && break
sleep 5
done
echo " $2:$1 $r"
}
nginx -t >/dev/null 2>&1 && systemctl reload nginx
systemctl reload dovecot
chk 443 "$D"; chk 465 "mail.$D"; chk 993 "mail.$D"; chk 995 "mail.$D"
ถ้าช่วงที่ตรวจยังเห็นใบเก่า อย่าเพิ่งสรุปว่าออกใบไม่สำเร็จ ให้รอตามรอบลองซ้ำก่อน
เรื่องเมลที่ผูกกับรายชื่อ SNI และ snidomains ผมเขียนแยกไว้แล้วใน ออกใบรับรองใหม่ให้เว็บ แล้ว SMTPS 465 ขึ้นชื่อโฮสต์ไม่ตรง บทความนี้เล่าแค่ว่าทำไมการต่ออายุถึงหยุด

เรื่องข้างเคียงที่เจอระหว่างทาง
มีโดเมนสองชื่อที่ SSL ถูกปิดไว้ใน DirectAdmin ตั้งแต่ต้น ขึ้นข้อความว่า SSL is not enabled เลยออกใบไม่ได้ ผมเปิดผ่าน API ได้ แต่ได้ผลข้างเคียงคือ CGI ของ user นั้นถูกเปิดไปด้วย เลยต้องกลับไปเปิดไฟล์ config ของ user ดูหลังทำทุกครั้ง
หลังจากนั้นผมเปิดตั้งค่าระดับเซิร์ฟเวอร์สองตัว คือ admin_ssl_install_to_missing=1 กับ admin_ssl_replace_all_expired_invalid=1 เพื่อให้ DirectAdmin ติดตั้งใบให้โดเมนที่ยังไม่มี และเปลี่ยนใบที่หมดอายุให้เอง ส่วนเว็บที่อยู่หลัง Cloudflare โหมด Full (strict) ต้องมีใบที่ถูกต้องที่ต้นทางด้วย เพราะ Cloudflare จะตรวจใบฝั่งเรา ไม่ใช่แค่ใบฝั่งผู้ใช้
เรื่องรายชื่อ NS ว่าชี้ไปที่ไหน ผมเคยเขียนในมุมตรวจจากฝั่งทะเบียนไว้แล้วใน nameserver ที่ registrar ไม่ตรงกับระบบบิลลิ่ง ตรงนี้ขอข้ามไป
ถ้าสั่งขอ wildcard อีกครั้งโดยที่ NS ยังอยู่ที่ Cloudflare มันจะล้มแบบเดิม เพราะ TXT ยังไปไม่ถึง Let's Encrypt การแก้ที่ถูกคือเปลี่ยนวิธีพิสูจน์ ไม่ใช่ขอซ้ำ อีกข้อคือ HTTP-01 ต้องเข้าพอร์ต 80 จากอินเทอร์เน็ตได้ และชื่อต้องชี้มาที่เครื่องนี้จริง ถ้าไม่ครบสองข้อนี้ ใบแบบระบุชื่อก็ออกไม่ได้เหมือนกัน และใบที่หมดอายุของโดเมนที่ไปอยู่ที่อื่นแล้ว ไม่ต้องไปแตะ
ขั้นตอนที่ผมจะใช้จากนี้
- รันสแกนอ่านอย่างเดียวทุกสัปดาห์ ดูใบที่เหลือน้อยกว่า 21 วันของทุกโดเมน
- ถามที่ resolver ภายนอกว่า A กับ NS ชี้ไปไหน ทำเฉพาะตัวที่ A ยังชี้มาที่เรา
- ออกใหม่แบบระบุชื่อด้วย helper สำรองของเดิมก่อนทุกครั้ง
- reload nginx กับ dovecot (และ exim ถ้ามีใบของตัวเอง) แล้วตรวจพอร์ต 443 465 993 995 พร้อมลองซ้ำ
- เช็กไฟล์ config ของ user ว่า helper ไม่ได้เปิดฟีเจอร์เพิ่มโดยไม่ตั้งใจ
สรุปคือ
wildcard ไม่ได้เป็นของเลวร้าย แต่มันผูกกับ DNS ของเครื่องที่ออกใบ และเงื่อนไขนี้ไม่มีใครจดไว้ที่ไหนเลย พอเงื่อนไขหายไปตอนย้าย NS ความผิดพลาดก็ไปโผล่เป็นเดือนต่อมา ส่วนตัวผมเลือกใช้ใบแบบระบุชื่อกับทุกโดเมนที่ลูกค้ายังต่อ DNS กับคนอื่นได้ และใช้ wildcard เฉพาะโดเมนที่ผมคุม NS เองแน่นอน ตามสะดวกครับ ถ้าใครคุม NS ไว้เองตลอด wildcard ก็ยังใช้ได้ปกติ
สิ่งที่ผมอยากทำต่อคือต่อสคริปต์สแกนเข้ากับการแจ้งเตือน จะได้ไม่ต้องรอให้ใครเป็นคนเจอ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย