ออกใบรับรองใหม่ให้เว็บ แล้ว SMTPS 465 ขึ้นชื่อโฮสต์ไม่ตรง
งานแตะใบรับรองที่อันตรายที่สุดคืองานที่ฝั่งเว็บผ่านครบทุกชื่อ เพราะมันทำให้เราปิดงานไปโดยไม่ได้เปิดดูพอร์ตเมลเลย ทั้งที่รายชื่อ SAN ชุดเดียวกันนั้นเป็นตัวกำหนดว่า exim กับ dovecot จะหยิบใบรับรองใบไหนมาตอบ
สรุปใน 1 นาที
- อาการ/ปัญหา
- ออกใบรับรองใหม่แล้วเว็บผ่านครบ แต่พอร์ต SMTPS 465 เสิร์ฟใบรับรองของตัวเครื่อง
- ใครควรอ่าน
- คนดูแล DirectAdmin ที่ขอ Let's Encrypt ให้โดเมนที่มีเมล
- ทางแก้แบบสั้น
- ขอผ่าน API ด้วยชุดชื่อเดียวกับ auto-SSL ให้มี mail, pop, smtp, ftp ใน snidomains สำรองก่อนแตะ และตรวจพอร์ตเมลผ่าน IP สาธารณะ
พอดีผมเปิดบัญชีใหม่ให้ลูกค้ารายหนึ่งบนเครื่อง shared host ที่ใช้ DirectAdmin โดเมนหลักมี alias (domain pointer) อีกชื่อหนึ่งชี้พื้นที่เดียวกัน ผมอยากให้ชื่อ alias เปิด https ได้ด้วย เลยขอใบรับรองใหม่โดยใส่ชื่อเว็บให้ครบ 4 ชื่อ ทดสอบเว็บผ่านหมด ผมก็นึกว่างานจบ จนไล่ตรวจพอร์ตเมลตามรายการที่ทำประจำถึงเห็นว่า mail.example.com:465 ตอบกลับมาด้วยใบรับรองของตัวเครื่อง ไม่ใช่ของโดเมน ถ้าลูกค้าตั้ง mail client แบบ SMTPS ไปแล้วจะเจอคำเตือนใบรับรองทันที
ชื่อโดเมน IP และชื่อ user ในบทความนี้เป็นค่าตัวอย่างทั้งหมด (example.com คือโดเมนหลัก example.net คือ alias 203.0.113.10 คือ IP สาธารณะของเครื่อง) ส่วนลำดับเหตุการณ์ คำสั่งตรวจ และผลที่ได้เป็นของจริง
อาการ: เว็บผ่านครบ 4 ชื่อ แต่พอร์ตเมลตอบด้วยใบรับรองของเครื่อง
ตัวเครื่องเป็น Debian ลง DirectAdmin ใช้ nginx กับ php-fpm ฝั่งเว็บ exim กับ dovecot ฝั่งเมล ตอนสร้างบัญชี DA ออกใบรับรองให้เองอัตโนมัติเป็นแบบ wildcard *.example.com รวมกับ apex ทุกอย่างใช้ได้ตั้งแต่วันแรก รวมทั้งเมล
ปัญหาเริ่มจากความต้องการที่ดูไม่มีพิษภัยเลยครับ เว็บของ alias ใช้ใบรับรองใบเดียวกับโดเมนหลัก ถ้าอยากให้ https://example.net ไม่ขึ้นเตือน ชื่อ example.net ต้องอยู่ในใบรับรองของ example.com ด้วย ผมเลยขอใบใหม่ผ่าน CMD_API_SSL แบบ request=letsencrypt ใส่ไป 4 ชื่อคือ example.com, www.example.com, example.net, www.example.net
ตรวจเว็บทีละชื่อก็ผ่านหมด
IP=203.0.113.10
for h in example.com www.example.com example.net www.example.net; do
printf '%-18s ' "$h"
openssl s_client -connect $IP:443 -servername $h -verify_hostname $h </dev/null 2>/dev/null \
| grep -E 'Verification'
done
# example.com Verification: OK
# www.example.com Verification: OK
# example.net Verification: OK
# www.example.net Verification: OK
ถ้าหยุดแค่นี้ผมคงส่งงานไปแล้ว แต่พอยิงคำสั่งเดียวกันไปที่พอร์ตเมล ผลออกมาคนละเรื่อง
openssl s_client -connect 203.0.113.10:465 -servername mail.example.com \
-verify_hostname mail.example.com </dev/null 2>/dev/null | grep -E 'subject=|Verification'
# subject=CN = host-a.example.org
# Verification error: hostname mismatch
subject ที่ได้คือชื่อ hostname ของเซิร์ฟเวอร์เอง แปลว่า exim ไม่รู้จักชื่อ mail.example.com ที่ไคลเอนต์ส่งมาใน SNI เลยหยิบใบรับรอง default ของเครื่องมาตอบ การเข้ารหัสยังทำงานอยู่ แต่ชื่อไม่ตรง ซึ่งสำหรับ mail client ส่วนใหญ่แปลว่าขึ้นหน้าต่างเตือน หรือไม่ยอมส่งเลย
ใบรับรองใบเดียวของโดเมนถูกใช้ทั้งกับ nginx, exim และ dovecot แต่แต่ละตัวมีวิธีเลือกใบของตัวเอง การทดสอบพอร์ต 443 ยืนยันได้แค่ฝั่งเว็บ ทุกครั้งที่แตะใบรับรองต้องยิง 465, 993, 995 ด้วยชื่อ mail. ของโดเมนนั้นต่ออีกรอบ ไม่อย่างนั้นคนที่เจอก่อนคือลูกค้า
ไฟล์ที่อยู่ตรงกลาง: /etc/virtual/snidomains
ฝั่งเมลของ DirectAdmin มีไฟล์ชื่อ /etc/virtual/snidomains ทำหน้าที่เป็นตารางจับคู่ ว่าชื่อโฮสต์ที่ไคลเอนต์ส่งมาใน SNI ต้องใช้ใบรับรองของ user ไหน โดเมนไหน พอผมเปิดดูก็เห็นสาเหตุทันที
grep example /etc/virtual/snidomains
# หลังขอ cert 4 ชื่อ (ผมย่อแต่ละแถวให้เหลือ ชื่อใน SNI : user : โดเมนเจ้าของ cert
# หน้าตาจริงของแถวอาจต่างไปตามรุ่นของ DA สิ่งที่ต้องดูคือชื่อทางซ้ายสุด)
# example.com:webuser:example.com
# www.example.com:webuser:example.com
# example.net:webuser:example.com
# www.example.net:webuser:example.com
มี 4 แถวตรงกับ 4 ชื่อที่ผมใส่ไปพอดี ไม่มีแถวของ mail. pop. smtp. ftp. เหลืออยู่เลย ทุกครั้งที่บันทึกใบรับรองผ่าน API ตัวนี้ DA จะสร้างรายการของโดเมนนั้นในไฟล์ใหม่จาก รายชื่อ SAN ที่เขียนตรง ๆ ในใบรับรอง ใบเดิมที่ DA ออกให้เองครอบชื่อเมลไว้ พอผมแทนด้วยใบที่มีแต่ชื่อเว็บ แถวของชื่อเมลก็หายไปทั้งชุด โดยไม่มีข้อความเตือนอะไรเลย
จุดที่ทำให้พลาดง่ายคือคำสั่งขอใบรับรองไม่ได้ล้มเหลว Let's Encrypt ออกให้ตามที่ขอ DA ติดตั้งให้ตามที่ได้ เว็บใช้ได้ตามที่ตั้งใจ ทุกขั้นรายงานว่าสำเร็จ สิ่งที่เสียไปคือของที่ผมไม่ได้ขอ แต่เคยมีอยู่
เดาผิดรอบแรก: ขอ wildcard กลับมาด้วยมือ
ความคิดแรกของผมคือ ใบเดิมเป็น wildcard แล้วมันใช้ได้ งั้นขอ wildcard กลับมาก็จบ ผมเลยรันสคริปต์ของ DA ด้วยมือ
cd /usr/local/directadmin/scripts
./letsencrypt.sh request '*.example.com,example.com'
ใบรับรองออกมาได้ แล้วผมก็ยิงพอร์ตเมลไล่ทีละพอร์ต
IP=203.0.113.10; H=mail.example.com
for p in 465 993 995; do
printf '%-4s ' "$p"
openssl s_client -connect $IP:$p -servername $H -verify_hostname $H </dev/null 2>/dev/null \
| grep -E 'Verification'
done
# 465 Verification error: hostname mismatch
# 993 Verification: OK
# 995 Verification: OK
dovecot ทั้ง 993 และ 995 กลับมาถูก แต่ exim ที่ 465 ยังผิดเหมือนเดิม สมมติฐานของผมที่ว่า wildcard กลับมาแล้วทุกอย่างจะกลับมาด้วยเลยผิดไปครึ่งหนึ่ง สาเหตุคือตัวสร้าง snidomains ของ DA จับคู่ให้เฉพาะชื่อที่เขียนตรง ๆ มันไม่ขยาย wildcard ออกเป็น mail. smtp. ให้ ใบรับรองครอบชื่อ mail.example.com ได้ก็จริง แต่ในตารางไม่มีแถวชื่อนั้น exim เลยไม่เคยไปถึงใบนั้น
ใบรับรองครอบ แต่ตารางไม่มีแถว
ขอผ่าน letsencrypt.sh ตรง ๆ ได้ *.example.com กลับมา dovecot 993/995 ถูก แต่ exim 465 ยังตอบด้วยใบของเครื่อง เพราะ snidomains ไม่มีชื่อ mail. ที่เขียนตรง ๆ ให้จับคู่ และชื่อ alias ก็ไม่ได้อยู่ในใบนี้
ชื่อครบ ตารางครบ
ขอผ่าน CMD_API_SSL โดยระบุ mail. pop. smtp. ftp. ลงไปตรง ๆ DA เขียน snidomains ให้เองครบทุกแถว exim กับ dovecot เจอชื่อที่ไคลเอนต์ส่งมา และ alias อยู่ในใบเดียวกันได้
มีอีกเรื่องที่ต้องรู้ไว้ก่อนจะคิดผสมสองแบบเข้าด้วยกัน ถ้าใส่ wildcard พร้อมกับชื่อที่ wildcard นั้นครอบอยู่แล้ว เช่น *.example.com คู่กับ mail.example.com Let's Encrypt จะปฏิเสธคำขอเพราะชื่อซ้ำซ้อน เลยต้องเลือกทางใดทางหนึ่ง และจากผลข้างบน ทางที่ใช้กับ exim ได้คือชื่อเฉพาะ
ทางแก้ที่จบ: ขอผ่าน API ด้วยชุดชื่อเดียวกับ auto-SSL
สิ่งที่ควรทำตั้งแต่แรกคือไปดูว่าตอน DA ออกใบให้เองมันใช้ชื่ออะไรบ้าง แล้วเลียนชุดนั้น คือ โดเมน, www., mail., pop., smtp., ftp. จากนั้นค่อยบวกชื่อที่เราต้องการเพิ่ม ซึ่งเคสนี้คือ apex กับ www. ของ alias รวมเป็น 8 ชื่อ
# ขอในฐานะเจ้าของโดเมน ผ่าน login key ของ admin (ค่า <LOGIN_KEY> อ่านจากไฟล์ที่ root เท่านั้นอ่านได้)
curl -s -u "admin|webuser:<LOGIN_KEY>" "https://host-a.example.org:2222/CMD_API_SSL" \
--data-urlencode "domain=example.com" \
--data-urlencode "action=save" \
--data-urlencode "type=create" \
--data-urlencode "request=letsencrypt" \
--data-urlencode "name=example.com" \
--data-urlencode "keysize=secp384r1" \
--data-urlencode "encryption=sha256" \
--data-urlencode "le_select0=example.com" \
--data-urlencode "le_select1=www.example.com" \
--data-urlencode "le_select2=mail.example.com" \
--data-urlencode "le_select3=pop.example.com" \
--data-urlencode "le_select4=smtp.example.com" \
--data-urlencode "le_select5=ftp.example.com" \
--data-urlencode "le_select6=example.net" \
--data-urlencode "le_select7=www.example.net"
ชื่อพารามิเตอร์ของ API ตัวนี้ขยับได้ตามรุ่นของ DA ใครจะเอาไปใช้ให้เทียบกับเอกสารของรุ่นที่ตัวเองใช้อยู่ก่อน หรือเปิดหน้า SSL ใน panel แล้วดูว่าฟอร์มส่งอะไรออกไป ส่วนตัวผมเลือกขอผ่าน API มากกว่ารันสคริปต์ตรง ๆ เพราะทาง API นั้น DA เป็นคนเขียน snidomains เองทั้งชุด ไม่ต้องไปแก้ไฟล์ด้วยมือ
ทุกชื่อในรายการต้องมี DNS ชี้มาที่เครื่องจริง ไม่อย่างนั้นการยืนยันของชื่อนั้นจะไม่ผ่านและคำขอทั้งใบล้ม ถ้าโดเมนอยู่หลัง proxy ของผู้ให้บริการ DNS อยู่ด้วย ให้ระวังเรื่อง record เมลที่ต้องไม่ผ่าน proxy ซึ่งผมเขียนไว้แล้วใน ย้ายโดเมนเข้า Cloudflare แล้วเมลหาย เป็นคนละสาเหตุกับเรื่องนี้ แต่ได้อาการปลายทางคล้ายกันคือเมลใช้ไม่ได้ทั้งที่เว็บปกติ
พอขอเสร็จก็กลับไปดูไฟล์เดิม
grep example /etc/virtual/snidomains | cut -d: -f1
# example.com
# www.example.com
# mail.example.com
# pop.example.com
# smtp.example.com
# ftp.example.com
# example.net
# www.example.net
แล้วยิงซ้ำทุกพอร์ต ได้ผลตามตารางนี้
| รอบ | 443 เว็บ 4 ชื่อ | 465 | 993 | 995 |
|---|---|---|---|---|
| cert 4 ชื่อเว็บ ผ่าน API | ตรงชื่อ | ไม่ตรง | ไม่ตรง | ไม่ตรง |
| wildcard ขอด้วยมือ | ไม่ได้วัด | ไม่ตรง | ตรงชื่อ | ตรงชื่อ |
| 8 ชื่อ ผ่าน API | ตรงชื่อ | ตรงชื่อ | ตรงชื่อ | ตรงชื่อ |
มีข้อสังเกตหนึ่งที่เจอระหว่างไล่ตรวจ ชื่อ webmail.example.com ขึ้นไม่ตรงเหมือนกัน แต่พอเช็กแล้วชื่อนี้ไม่มี DNS record บนเครื่องนี้ และเป็นแบบเดียวกันกับทุกโดเมนบนเครื่องอยู่ก่อนแล้ว ไม่ใช่ของที่พังเพราะงานรอบนี้ ผมจดไว้เฉย ๆ เพื่อไม่ให้ไปไล่แก้ผิดเรื่อง
กลไกเดียวกันใช้กับทุกชื่อ ตารางมีแถวเฉพาะชื่อที่เขียนลงใบรับรองตรง ๆ ถ้าจะให้ mail client ตั้งค่าด้วยชื่อ mail. ของโดเมน alias ต้องใส่ชื่อนั้นลงในชุด SAN เองเช่นกัน และต้องมี DNS ของชื่อนั้นชี้มาที่เครื่องก่อนขอ
กับดักตอนทดสอบ: 127.0.0.1 คืนใบ default ทุกครั้ง
ระหว่างไล่ตรวจผมเกือบสรุปผิดไปอีกเรื่อง ตอนแรกผมยิงทดสอบจากในตัวเซิร์ฟเวอร์เองไปที่ 127.0.0.1:443 แล้วได้ใบรับรอง default ของเครื่องกลับมาทุกชื่อ ดูเหมือนใบที่เพิ่งขอไม่ได้ถูกติดตั้ง ซึ่งไม่จริง nginx บนเครื่องนี้ bind vhost ของแต่ละโดเมนไว้ที่ IP สาธารณะ คำขอที่เข้าทาง loopback เลยไม่ตรง vhost ไหนและตกไปที่ตัว default เสมอ
# ผิด: ได้ cert default เสมอ ไม่ว่าจะติดตั้งถูกหรือไม่
openssl s_client -connect 127.0.0.1:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject
# ถูก: ชี้ไปที่ IP สาธารณะ หรือบังคับ resolve ให้ curl
openssl s_client -connect 203.0.113.10:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName
curl -s --resolve example.com:443:203.0.113.10 -o /dev/null \
-w '%{http_code} ssl_verify=%{ssl_verify_result}\n' https://example.com/
กับดักเดียวกันนี้ผมเคยเจอมาแล้วตอนย้ายโดเมนทั้งก้อน เล่าไว้ใน โดเมน .com โดนยึด ย้ายทั้งเว็บไป .net บน DirectAdmin รอบนั้นเป็นฝั่งเว็บอย่างเดียว รอบนี้เพิ่มมาอีกชั้นคือพอร์ตเมล ซึ่งใช้หลักเดียวกันทุกประการ ต้องระบุ -servername ให้ตรงชื่อที่ลูกค้าจะใช้จริง และต้องยิงผ่าน IP ที่ลูกค้าจะมาถึงจริง
-servername คือค่าที่ส่งเป็น SNI ถ้าไม่ใส่ เซิร์ฟเวอร์ไม่รู้ว่าเราถามถึงโดเมนไหนและจะตอบด้วยใบ default ส่วน -verify_hostname คือตัวที่ทำให้ openssl เทียบชื่อในใบรับรองให้ แล้วพิมพ์ Verification: OK หรือ hostname mismatch ออกมาตรง ๆ ไม่ต้องอ่าน SAN ด้วยตาเอง
สำรองก่อนแตะ และรายการตรวจหลังแตะ
ก่อนเปลี่ยนใบรับรองทุกครั้งผมสำรองไฟล์สี่ตัวของโดเมนนั้นกับไฟล์ snidomains ไว้ก่อน เพราะถ้าคำขอใหม่ล้มกลางทาง หรือได้ใบที่ชื่อไม่ครบ การย้อนกลับด้วยไฟล์เดิมเร็วกว่าการขอใหม่มาก และไม่กินโควตาการออกใบของ Let's Encrypt
D=/usr/local/directadmin/data/users/webuser/domains
B=/root/cert-backup-$(date +%F)
mkdir -p "$B" && chmod 700 "$B"
cp -a "$D"/example.com.cert "$D"/example.com.key \
"$D"/example.com.cacert "$D"/example.com.cert.combined "$B"/
cp -a /etc/virtual/snidomains "$B"/
ls -l "$B"
ส่วนรายการตรวจที่ผมใช้หลังแตะใบรับรองของโดเมนไหนก็ตามบนเครื่อง DirectAdmin ตอนนี้เป็นแบบนี้
- สำรอง
.cert.key.cacert.cert.combinedของโดเมน และ/etc/virtual/snidomainsลงโฟลเดอร์ใต้/rootที่ตั้งสิทธิ์ 700 - เตรียมชุดชื่อ: โดเมน,
www.,mail.,pop.,smtp.,ftp.แล้วค่อยบวก apex กับwww.ของ alias ถ้ามี ห้ามผสม wildcard กับชื่อที่มันครอบอยู่ - ขอผ่าน
CMD_API_SSLไม่ใช่รันสคริปต์ตรง ๆ เพื่อให้ DA เขียน snidomains เอง grepชื่อโดเมนใน snidomains ต้องเห็นแถวของชื่อเมลครบ- ยิง
openssl s_clientผ่าน IP สาธารณะ ที่พอร์ต 443 ทุกชื่อเว็บ และ 465, 993, 995 ด้วยชื่อmail.ต้องได้Verification: OKทุกบรรทัด - ชื่อที่ไม่ตรงอยู่ก่อนแล้ว เช่น
webmail.ที่ไม่มี DNS ให้จดแยกไว้ ไม่นับเป็นของที่พังจากงานรอบนี้
ตัวเลขของงานนี้
สรุปคือรายชื่อ SAN บนเครื่อง DirectAdmin ไม่ได้เป็นแค่รายชื่อที่ใบรับรองครอบ มันเป็นข้อมูลตั้งต้นของตารางที่ exim ใช้เลือกใบด้วย การขอใบใหม่ด้วยชื่อน้อยกว่าเดิมเลยเท่ากับการลบแถวออกจากตารางนั้นโดยไม่รู้ตัว ผมเสียไปสามรอบของการขอใบกว่าจะได้ชุดที่ถูก ทั้งที่ถ้าเปิดดูใบเดิมก่อนว่ามีชื่ออะไรบ้างก็จบในรอบเดียว ใครดูแลเครื่องลักษณะเดียวกันจะเอาลูปตรวจพอร์ตข้างบนไปใส่ในขั้นตอนของตัวเองก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



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