วิธีสร้าง Self-signed Certificate บน Ubuntu 24.04 ให้ผ่าน
สร้างใบรับรอง TLS ที่ Chrome ยอมรับบน Ubuntu 24.04 ด้วยคำสั่ง OpenSSL พร้อมการตั้งค่า SAN ที่ถูกต้อง การเชื่อมต่อกับ Nginx หรือ Apache และวิธีเพิ่มความเชื่อถือโดยไม่ต้องใช้ curl -k
สิ่งที่คุณกำลังสร้าง
ใบรับรอง TLS แบบ self-signed ที่เบราว์เซอร์และไคลเอนต์สมัยใหม่ยอมรับได้จริง รวมถึงการตั้งค่า subjectAltName ที่ถูกต้อง การกำหนดสิทธิ์ของคีย์ที่เหมาะสม และการเชื่อมต่อเข้ากับ Nginx หรือ Apache นอกจากนี้ยังมีส่วนที่คู่มือส่วนใหญ่ข้ามไป คือการทำให้ไคลเอนต์ของคุณ เชื่อถือ ใบรับรองอย่างถูกต้อง แทนที่จะต้องกดข้ามคำเตือนหรือฝังค่า curl -k ลงในสคริปต์ตลอดไป ในตอนท้าย คุณจะได้เรียนรู้วิธีสร้าง Private CA ด้วยคำสั่งเพียง 5 คำสั่ง สำหรับกรณีที่บริการภายในของคุณเพิ่มจำนวนขึ้น
ก่อนอื่นต้องตัดสินใจก่อน เพราะใบรับรองแบบ self-signed มักถูกนำไปใช้ในสถานการณ์ที่ไม่เหมาะสม หากบริการของคุณสามารถเข้าถึงได้จากอินเทอร์เน็ตสาธารณะผ่านชื่อ DNS จริง ให้หยุดอ่านบทความนี้แล้วไปใช้ ใบรับรองฟรีจาก Let's Encrypt ด้วย certbot บน Nginx หรือ วิธีเดียวกันบน Apache แทน ซึ่งไม่มีค่าใช้จ่าย ต่ออายุอัตโนมัติ และเบราว์เซอร์หรือระบบปฏิบัติการหลักๆ ให้ความเชื่อถืออยู่แล้ว การใช้ใบรับรองแบบ self-signed บนเว็บไซต์สาธารณะเป็นการฝึกให้ผู้ใช้ของคุณกดข้ามคำเตือนด้านความปลอดภัย ซึ่งเป็นนิสัยที่แย่ยิ่งกว่าการใช้ HTTP ธรรมดาเสียอีก
ใบรับรองแบบ self-signed เป็นเครื่องมือที่เหมาะสมเมื่อไม่มีอินเทอร์เน็ตสาธารณะเข้ามาเกี่ยวข้อง เช่น แผงควบคุมผู้ดูแลระบบที่ผูกไว้กับ ที่อยู่ WireGuard tunnel บน VPS ของคุณ, เครื่อง staging บนเครือข่ายส่วนตัว, การรับส่งข้อมูลระหว่าง backend, อุปกรณ์ใน home-lab หรือการเปลี่ยนใบรับรองเริ่มต้นที่ Webmin สร้างขึ้นเองบนพอร์ต 10000 ทั้งนี้ Let's Encrypt ไม่สามารถออกใบรับรองให้กับ 10.8.0.1 หรือ git.internal.lan ได้ และไม่มี CA สาธารณะที่ไหนจะออกใบรับรองให้กับ IP ส่วนตัวหรือ TLD ที่ตั้งขึ้นเอง สำหรับชื่อเหล่านี้ คุณคือ CA ด้วยตัวคุณเอง
ทุกขั้นตอนด้านล่างนี้ทำงานบน Ubuntu 24.04 ที่ติดตั้งใหม่ ซึ่งมาพร้อมกับ OpenSSL 3.0.x (ใช้ openssl version เพื่อตรวจสอบ) เนื้อหาทั้งหมดนี้ไม่จำเป็นต้องเชื่อมต่ออินเทอร์เน็ต และสามารถทำงานได้ในสภาพแวดล้อมแบบ air-gapped
เหตุใดคำสั่งบรรทัดเดียวแบบเก่าจึงสร้างใบรับรองที่ Chrome ปฏิเสธ
คำสั่งที่บทเรียนก่อนปี 2017 มักแนะนำให้ใช้มีหน้าตาดังนี้:
# Do not run this: shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout selfsigned.key -out selfsigned.crtคำสั่งนี้จะถามคำถามเชิงโต้ตอบหลายข้อ โดยนำชื่อโฮสต์ของคุณไปใส่ในฟิลด์ Common Name และสร้างใบรับรองที่ไม่มีส่วนขยาย subjectAltName ใบรับรองดังกล่าวจึงใช้งานไม่ได้ตั้งแต่ต้น Chrome เลิกอ่านค่า Common Name ตั้งแต่เวอร์ชัน 58 ในเดือนเมษายน 2017 ส่วน RFC 2818 ได้ยกเลิกการจับคู่ด้วย CN มาตั้งแต่ปี 2000 แล้ว ทั้ง Firefox และ Safari ก็มีพฤติกรรมเช่นเดียวกัน แม้ว่า curl และโมดูล ssl ของ Python จะยังคงถอยกลับไปใช้ Common Name ในกรณีที่ใบรับรองไม่มี SAN เลยก็ตาม ซึ่งเป็นสาเหตุที่ทำให้ใบรับรองที่ไม่มี SAN สามารถผ่านการทดสอบ curl แบบเร็วๆ ได้ แต่กลับใช้งานไม่ได้ในเบราว์เซอร์ทุกตัว สำหรับเบราว์เซอร์แล้ว ใบรับรองจะระบุตัวตนของเซิร์ฟเวอร์ผ่านส่วนขยาย SAN เท่านั้น หากไม่มีส่วนขยายนี้ เบราว์เซอร์จะแจ้งเตือนคุณด้วยข้อความนี้:
NET::ERR_CERT_COMMON_NAME_INVALID
This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.การปรับแต่ง trust-store ไม่สามารถแก้ไขข้อผิดพลาดนี้ได้ เนื่องจากใบรับรองไม่ได้ระบุชื่อใดๆ ไว้เลย หากคุณกำลังเผชิญกับ NET::ERR_CERT_COMMON_NAME_INVALID ในขณะนี้ แสดงว่าใบรับรองของคุณไม่มี SAN (หรือระบุไว้ไม่ถูกต้อง) และคุณจำเป็นต้องสร้างใบรับรองใหม่ โชคดีที่การแก้ไขสามารถทำได้ด้วยคำสั่งเดียว
การออกใบรับรองที่เบราว์เซอร์ยอมรับ: คำสั่งเดียว
OpenSSL ได้เพิ่มแฟล็ก -addext เข้ามาตั้งแต่เวอร์ชัน 1.1.1 ซึ่งทำให้คุณไม่จำเป็นต้องใช้วิธีการยุ่งยากในการสร้างไฟล์ config เพื่อใส่ SAN เหมือนคู่มือเก่าๆ อีกต่อไป บน Ubuntu 24.04 ให้ใช้คำสั่งดังนี้:
sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
-keyout /etc/ssl/private/git.internal.key \
-out /etc/ssl/certs/git.internal.crt \
-subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"คำอธิบายการทำงานของแต่ละแฟล็ก:
-x509สร้างใบรับรองแบบ self-signed โดยตรงแทนที่จะสร้างคำขอลงนาม (signing request)-newkey rsa:4096สร้างคีย์ใหม่ในขั้นตอนเดียวกัน RSA 4096 สามารถใช้งานร่วมกับไคลเอนต์รุ่นเก่าได้โดยไม่มีปัญหา หากทุกบริการที่เชื่อมต่อเป็นระบบสมัยใหม่ทั้งหมด การใช้-newkey ec -pkeyopt ec_paramgen_curve:P-256จะมีขนาดเล็กและทำงานได้เร็วกว่า-noencเป็นรูปแบบคำสั่งของ OpenSSL 3.x ที่ใช้แทน-nodesแบบเดิม ซึ่งหมายถึงการไม่ใส่รหัสผ่านให้กับคีย์ ทั้งสองรูปแบบสามารถใช้งานได้ การใช้คีย์ที่มีรหัสผ่านจะทำให้ nginx ค้างเพื่อรอรับอินพุตทุกครั้งที่บูตเครื่อง ดังนั้นสำหรับคีย์ของเซิร์ฟเวอร์ คุณควรใช้ตัวเลือกนี้-days 730กำหนดอายุ 2 ปี; รายละเอียดเพิ่มเติมเกี่ยวกับตัวเลขนี้อยู่ในส่วนของวันหมดอายุ-subjใช้สำหรับตอบคำถามแบบโต้ตอบผ่านบรรทัดคำสั่ง แม้ว่าปัจจุบันค่า CN จะเป็นเพียงข้อมูลประกอบ แต่ก็ควรตั้งค่าให้เป็นชื่อหลักไว้ เนื่องจากเครื่องมือบางอย่างยังคงแสดงผลค่านี้อยู่-addext "subjectAltName=..."เป็นแฟล็กที่สำคัญที่สุด ให้ระบุ ทุกชื่อ และ ทุก IP ที่ไคลเอนต์จะใช้พิมพ์เข้าถึง: ใช้รายการDNS:สำหรับชื่อโฮสต์ (สามารถใช้ wildcard เช่นDNS:*.internal.lanได้) และรายการIP:สำหรับที่อยู่ IP หากมีผู้ใช้งานเข้าถึงผ่านhttps://10.8.0.1จะต้องระบุรายการIP:10.8.0.1ไว้ด้วย มิฉะนั้นการมีเพียง DNS SAN จะทำให้ผู้ใช้พบข้อผิดพลาดNET::ERR_CERT_COMMON_NAME_INVALIDตลอดเวลา
ตรวจสอบให้แน่ใจว่า SAN ถูกเพิ่มเข้าไปจริงก่อนนำไปใช้งาน:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltNameผลลัพธ์ที่ถูกต้อง:
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1หากผลลัพธ์ที่ได้แสดงข้อความ No extensions in certificate แสดงว่าใบรับรองไม่มี SAN และเบราว์เซอร์จะปฏิเสธการเชื่อมต่อ ให้ทำการสร้างใบรับรองใหม่แทนที่จะดำเนินการต่อ
การจำกัดสิทธิ์เข้าถึง private key
private key ที่ผู้ใช้ทุกคนบนเซิร์ฟเวอร์สามารถอ่านได้นั้นถือว่าไม่ใช่ private key อีกต่อไป บนระบบ Ubuntu 24.04 มาตรฐาน ไดเรกทอรี /etc/ssl/private จะถูกตั้งค่าเป็น 700 root:root ไว้แล้ว ทำให้มีเพียง root เท่านั้นที่สามารถแสดงรายการไฟล์ภายในได้ โดยจะเปลี่ยนเป็น 710 root:ssl-cert ก็ต่อเมื่อมีการติดตั้งแพ็กเกจ ssl-cert เข้ามาในระบบ (ซึ่ง apache2 แนะนำให้ติดตั้ง) ไม่ว่ากรณีใดก็ตาม ให้กำหนดสิทธิ์ของไฟล์ด้วยตนเองอย่างชัดเจน:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keyทั้ง nginx และ Apache จะอ่าน certificate ในฐานะ root ก่อนที่จะลดระดับสิทธิ์ลง ดังนั้นการตั้งค่า root:root ด้วยโหมด 600 จึงใช้งานได้กับบริการเหล่านี้ หาก private key นั้นใช้สำหรับบริการที่รันด้วยผู้ใช้เฉพาะของตนเองและโหลด key ด้วยตัวเอง เช่น แอป Node, Gitea, Python daemon ให้ใช้ chown สิทธิ์ความเป็นเจ้าของไฟล์ให้กับผู้ใช้บริการนั้นแทน โดยยังคงใช้โหมด 600 เช่นเดิม สิ่งที่ไม่ควรทำโดยเด็ดขาดคือ: การตั้งค่าโหมด 644, การเก็บสำเนาไว้ใน git repository หรือการเก็บสำเนาไว้ใน /tmp
เชื่อมต่อเข้ากับ nginx
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name git.internal.lan;
ssl_certificate /etc/ssl/certs/git.internal.crt;
ssl_certificate_key /etc/ssl/private/git.internal.key;
location / {
proxy_pass http://127.0.0.1:3000;
}
}sudo nginx -t && sudo systemctl reload nginxnginx -t จะต้องแสดงผลเป็น syntax is ok และ test is successful ก่อนที่คำสั่ง reload จะทำงาน หากระบบแสดงผลเป็น SSL_CTX_use_PrivateKey_file() failed ... key values mismatch แสดงว่า certificate และ key มาจากการสร้างคนละรอบกัน โปรดดูรายละเอียดในส่วน failure-modes
เชื่อมต่อเข้ากับ Apache
sudo a2enmod ssl proxy proxy_httpssl เพียงอย่างเดียวไม่เพียงพอในกรณีนี้: vhost ด้านล่างใช้ ProxyPass และหากไม่มี mod_proxy และ mod_proxy_http การทดสอบ config จะล้มเหลวด้วย Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration ให้บันทึก vhost เป็น /etc/apache2/sites-available/git-internal.conf:
<VirtualHost *:443>
ServerName git.internal.lan
SSLEngine on
SSLCertificateFile /etc/ssl/certs/git.internal.crt
SSLCertificateKeyFile /etc/ssl/private/git.internal.key
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2configtest ควรตอบกลับด้วย Syntax OK จากนั้นให้ทดสอบจากเครื่องไคลเอนต์:
curl -v https://git.internal.lan/และคุณจะได้รับข้อผิดพลาด:
curl: (60) SSL certificate problem: self-signed certificateนั่นไม่ใช่บั๊ก แต่เป็นการทำงานของ TLS ตามปกติ: curl ไม่รู้จักใบรับรองของคุณและปฏิเสธที่จะสื่อสารกับเซิร์ฟเวอร์ที่ไม่สามารถยืนยันตัวตนได้ ส่วนถัดไปคือวิธีแก้ไขที่แท้จริง และไม่ใช่สิ่งที่คนส่วนใหญ่บนอินเทอร์เน็ตทำกันอยู่ในขณะนี้
การทำให้ไคลเอนต์เชื่อถือใบรับรองและรูปแบบการแก้ไขที่ควรหลีกเลี่ยง
เริ่มจากวิธีแก้ไขที่ผิดและระบุว่าทำไมถึงผิด การใช้ curl -k (หรือ --insecure) ในสคริปต์, verify=False ใน Python requests หรือ NODE_TLS_REJECT_UNAUTHORIZED=0 ใน Node ไม่ได้ทำให้ใบรับรองของคุณได้รับความเชื่อถือ สิ่งเหล่านี้เป็นการปิดการตรวจสอบใบรับรอง ซึ่งหมายความว่าไคลเอนต์จะยอมสื่อสารกับเซิร์ฟเวอร์ใดก็ตามที่แสดงใบรับรองใดก็ตาม รวมถึงใบรับรองที่ผู้โจมตีนำมาวางไว้ระหว่างทาง คุณยังคงต้องแบกรับภาระของ TLS แต่กลับสูญเสียการยืนยันตัวตนซึ่งเป็นจุดประสงค์หลักของมัน ที่แย่กว่านั้นคือแฟล็กเหล่านี้มักจะแพร่กระจายไปเรื่อยๆ จากการคัดลอกลงใน cron job, สคริปต์ deploy และโค้ดใน production จนไม่มีใครจำได้ว่าการเชื่อมต่อใดที่ควรจะเป็นเพียงชั่วคราว หาก verify=False ยังคงอยู่หลังจากจบช่วงการดีบั๊ก แสดงว่าการออกแบบนั้นผิดพลาด
วิธีแก้ไขที่ถูกต้องคือการสอนให้ OS ของไคลเอนต์แต่ละเครื่องเชื่อถือใบรับรองนี้ในฐานะ root ที่เชื่อถือได้ สำหรับไคลเอนต์ Ubuntu และ Debian:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesบรรทัดที่สำคัญในผลลัพธ์ (ตามด้วยบล็อก Running hooks in /etc/ca-certificates/update.d...):
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.มีข้อควรระวังสองประการที่ซ่อนอยู่ในบรรทัดเหล่านั้น ไฟล์ ต้อง ลงท้ายด้วย .crt หากใช้นามสกุล .pem ระบบจะเพิกเฉยโดยไม่แจ้งเตือนและคุณจะได้รับ 0 added โดยไม่มีข้อความแสดงข้อผิดพลาด และเนื้อหาต้องเป็นรูปแบบ PEM โดยไฟล์ต้องขึ้นต้นด้วย -----BEGIN CERTIFICATE----- หากเป็นไฟล์ไบนารี DER ให้แปลงก่อนด้วย openssl x509 -inform der -in file.der -out file.crt การเพิ่มใบรับรองที่ลงนามด้วยตนเอง (self-signed) เข้าไปในฐานะ root นั้นใช้ได้ผล เพราะใบรับรองที่ลงนามด้วยตนเองถือเป็น root ของตัวมันเอง
หลังจากนั้น curl, wget, git, apt และโปรแกรมอื่นๆ ที่ใช้ OpenSSL ร่วมกับ system bundle จะเชื่อถือเซิร์ฟเวอร์โดยไม่ต้องใช้แฟล็กใดๆ เพิ่มเติม ไคลเอนต์บางตัวมีที่เก็บใบรับรอง (trust store) ของตนเองและต้องการการตั้งค่าเฉพาะ:
- Chrome/Chromium บน Linux อ่านฐานข้อมูล NSS ไม่ใช่ system store: ใช้
sudo apt install libnss3-toolsจากนั้นใช้certutil -d sql:$HOME/.local/share/pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtต่อผู้ใช้หนึ่งราย พาธนั้นเป็นค่าเริ่มต้นของ Chrome ตั้งแต่เวอร์ชัน 146 หากผู้ใช้ยังมี~/.pki/nssdbแบบเก่าจากการติดตั้งเดิม Chrome จะยังคงอ่านไฟล์นั้นแทน ดังนั้นให้ชี้-dไปที่sql:$HOME/.pki/nssdbบนเครื่องเหล่านั้น - Firefox มีที่เก็บใบรับรองของตนเอง: ไปที่ Settings → Privacy & Security → Certificates → Import สำหรับ Windows และ macOS คุณสามารถเปลี่ยนค่า
security.enterprise_roots.enabledเป็นtrueในabout:configเพื่อให้มันอ่านจาก OS store ได้ Mozilla ระบุการตั้งค่านี้สำหรับ Windows และ macOS เท่านั้น ไม่รวม Linux ดังนั้นบน Ubuntu ให้ใช้วิธีการนำเข้า (import) - Python requests ใช้ CA bundle ของตนเอง (certifi) และเพิกเฉยต่อ system store: ให้ส่งค่า
verify="/usr/local/share/ca-certificates/git.internal.crt"หรือ export ค่าREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt - Node.js ใช้ root ที่มากับตัวโปรแกรมเป็นค่าเริ่มต้น: ให้ export ค่า
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crtหรือตั้งค่าให้มันอ่านจาก system store ที่คุณเพิ่งอัปเดตด้วยแฟล็ก--use-system-ca(Node 23.9 ขึ้นไปบน Linux) หรือNODE_USE_SYSTEM_CA=1(Node 24.6 และ 22.19 ขึ้นไป)
บนไคลเอนต์ Windows ให้ดับเบิลคลิกที่ .crt และติดตั้งลงใน Trusted Root Certification Authorities; บน macOS ให้เพิ่มลงใน System keychain ใน Keychain Access และตั้งค่าเป็น Always Trust
การใช้ Root CA เดียวสำหรับหลายบริการ: การสร้าง Private CA ขนาดเล็ก
การจัดการความเชื่อถือ (trust) แยกรายใบรับรองจะทำให้ระบบขยายตัวได้ยากทันที หากมี 6 บริการและเครื่องลูกข่าย 4 เครื่อง คุณจะต้องติดตั้งความเชื่อถือถึง 24 ครั้ง และทุกบริการใหม่ที่เพิ่มเข้ามาก็จะยิ่งเพิ่มภาระมากขึ้น วิธีแก้คือการใช้ Private CA โดยให้เครื่องลูกข่ายเชื่อถือ หนึ่ง root certificate เท่านั้น แล้วคุณจึงค่อยลงนามใบรับรองของแต่ละบริการด้วย CA นั้น
ตัวเลือกที่ใช้งานง่ายคือ mkcert ซึ่งมีอยู่ในคลังซอฟต์แวร์ของ Ubuntu 24.04 และสามารถจัดการ NSS store (สำหรับ Firefox และ Chrome โดยมีข้อควรระวังเล็กน้อยด้านล่าง) ซึ่ง update-ca-certificates ไม่สามารถทำได้:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install จะสร้าง root ขึ้นมาและลงทะเบียนไว้ใน system store, NSS store ของเบราว์เซอร์ที่ค้นพบ และ Java store เมื่อมีการตั้งค่า JAVA_HOME สำหรับ Node นั้นไม่ได้อ่านค่าจาก system store เว้นแต่จะมีการกำหนดค่าไว้ ดังนั้นให้เริ่มแอป Node ด้วย --use-system-ca (หรือ NODE_USE_SYSTEM_CA=1) ตามที่ระบุไว้ข้างต้น หรือทำการ export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" คำสั่งที่สามจะสร้าง git.internal.lan+2.pem และ git.internal.lan+2-key.pem ซึ่งพร้อมสำหรับการนำไปใส่ในส่วนของ nginx หรือ Apache ตามที่กล่าวไปก่อนหน้านี้ การออกแบบของเครื่องมือนี้ตั้งอยู่บนสมมติฐานว่าเป็นเครื่องสำหรับพัฒนา โดยที่ root key จะถูกเก็บไว้ในเครื่องที่รัน -install ดังนั้นจึงเหมาะสำหรับแล็ปท็อปของนักพัฒนา แต่ไม่เหมาะสมสำหรับกลุ่มเซิร์ฟเวอร์
ข้อควรระวัง ณ เดือนตุลาคม 2026: mkcert จะค้นหาฐานข้อมูล NSS ของ Chrome ที่ path เดิมคือ ~/.pki/nssdb เท่านั้น ไม่ใช่ที่ ~/.local/share/pki/nssdb ซึ่งเป็นค่าเริ่มต้นของ Chrome ตั้งแต่เวอร์ชัน 146 เป็นต้นไป ในเครื่องที่ไม่มีฐานข้อมูลแบบเดิม คุณต้องเพิ่ม root สำหรับ Chrome ด้วยตนเองโดยใช้คำสั่ง certutil จากส่วนก่อนหน้า โดยใช้ -i "$(mkcert -CAROOT)/rootCA.pem"
สำหรับเซิร์ฟเวอร์ การใช้ OpenSSL แบบปกติสามารถสร้าง CA ได้ครบถ้วนด้วย 5 คำสั่ง:
openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
-out lab-ca.crt -subj "/CN=Lab Internal CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
-CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crtกับดักสำคัญอยู่ที่คำสั่งสุดท้าย: openssl x509 -req จะตัดส่วนขยาย (extensions) ทั้งหมดออกจาก CSR โดยค่าเริ่มต้น รวมถึง SAN ที่คุณตั้งใจเพิ่มเข้าไปด้วย -copy_extensions copy (ซึ่งเป็นตัวเลือกของ OpenSSL 3.x จึงใช้งานได้บน 24.04) จะช่วยคัดลอกส่วนขยายเหล่านั้นมาด้วย หากละเว้นตัวเลือกนี้ ใบรับรองที่ลงนามแล้วจะไม่มี SAN และ Chrome จะแสดงข้อผิดพลาด NET::ERR_CERT_COMMON_NAME_INVALID อีกครั้ง ให้ตรวจสอบด้วยคำสั่ง openssl x509 -noout -ext subjectAltName เหมือนเดิม
แจกจ่าย lab-ca.crt ไปยังเครื่องลูกข่ายผ่านขั้นตอนการเพิ่ม trust-store ที่กล่าวไปข้างต้น โดยทำเพียงครั้งเดียวต่อหนึ่งเครื่องเท่านั้น จงปกป้อง lab-ca.key ให้ดีเสมือนสมบัติล้ำค่า โดยกำหนดสิทธิ์เป็น 600 และควรเก็บไว้ในเครื่องที่ไม่ใช่หนึ่งในเซิร์ฟเวอร์ที่ใบรับรองนั้นลงนามให้ เพราะผู้ที่ถือครองไฟล์นี้จะสามารถสร้างใบรับรองสำหรับชื่อใดก็ได้ที่เครื่องลูกข่ายของคุณจะยอมรับว่าเป็นของจริง
การหมดอายุและการหมุนเวียนใบรับรอง
อายุการใช้งานของใบรับรองจาก Public CA กำลังสั้นลงเรื่อยๆ โดย CA/Browser Forum ได้กำหนดเพดานอายุใบรับรองที่ออกใหม่และได้รับความเชื่อถือในระดับสาธารณะไว้ที่ 200 วันในเดือนมีนาคม 2026 (ลดลงจาก 398 วัน) และจะลดลงเหลือ 100 วันในปี 2027 จนถึง 47 วันภายในเดือนมีนาคม 2029 อย่างไรก็ตาม กฎเหล่านี้มีผลบังคับใช้เฉพาะกับ CA ที่ได้รับความเชื่อถือในระดับสาธารณะเท่านั้น CA ส่วนตัวของคุณไม่ได้อยู่ภายใต้กฎดังกล่าว และเบราว์เซอร์ก็ไม่ได้บังคับใช้กฎเหล่านี้กับ root certificate ที่ติดตั้งด้วยตนเอง แต่มีข้อจำกัดหนึ่งที่ใช้งานจริงคือ แพลตฟอร์มของ Apple จะปฏิเสธใบรับรอง TLS ของเซิร์ฟเวอร์ที่มีอายุเกิน 825 วันไม่ว่าใครจะเป็นผู้ออกให้ก็ตาม ดังนั้นหากมีการเชื่อมต่อจาก iPhone หรือ Mac ควรจำกัดอายุของ leaf certificate ไว้ไม่เกินสองปี -days 730 สามารถผ่านเกณฑ์นี้ได้ทุกที่ การใช้ root certificate ที่มีอายุสิบปีร่วมกับ leaf certificate ที่มีอายุสองปีถือเป็นรูปแบบที่เหมาะสมสำหรับการใช้งานภายใน
ใบรับรองที่มีอายุการใช้งานยาวนานมักจะล้มเหลวในรูปแบบเดียวคือ การหยุดทำงานอย่างเงียบๆ พร้อมกันทั้งหมดในวันที่ไม่มีใครจำได้ว่าเลือกไว้ ให้ตรวจสอบสิ่งที่คุณมีดังนี้:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateควรบันทึกการต่ออายุไว้ในปฏิทินจริง หรือตั้งค่า cron ให้แจ้งเตือนล่วงหน้า 30 วัน โดย openssl x509 -checkend 2592000 -in cert.crt จะส่งค่า exit code ที่ไม่ใช่ศูนย์เมื่อใบรับรองใกล้หมดอายุภายในจำนวนวินาทีที่กำหนด หากคุณใช้งาน Uptime Kuma สำหรับการตรวจสอบสถานะ อยู่แล้ว ตัวตรวจสอบ HTTPS ของเครื่องมือนี้จะแจ้งเตือนเมื่อใบรับรองใกล้หมดอายุให้โดยอัตโนมัติ
การหมุนเวียนใบรับรองด้วย CA ส่วนตัวนั้นเป็นเรื่องที่เรียบง่าย: เพียงรันคำสั่ง CSR และลงนามใหม่ สลับไฟล์ และโหลด web server ใหม่อีกครั้ง เนื่องจาก root certificate ไม่ได้เปลี่ยนแปลง ไคลเอนต์จึงไม่พบความผิดปกติใดๆ
รูปแบบความล้มเหลวและข้อความที่คุณจะพบ
NET::ERR_CERT_AUTHORITY_INVALID คือสถานะที่ คาดการณ์ไว้ ก่อนที่คุณจะติดตั้งความเชื่อถือ (trust) ไม่ใช่ข้อบกพร่องของใบรับรอง หากปัญหายังคงอยู่ หลังจาก ที่คุณติดตั้ง root แล้ว: บน Linux นั้น Chrome จะอ่านค่าจาก NSS แทนที่จะเป็น system store (ดูขั้นตอน certutil) และตั้งแต่ Chrome 146 เป็นต้นมา ฐานข้อมูลดังกล่าวจะอยู่ที่ ~/.local/share/pki/nssdb เว้นแต่จะมี ~/.pki/nssdb แบบเดิมอยู่ ซึ่งทำให้ root ที่เพิ่มเข้าไปใน path อื่นไม่ถูกมองเห็น หรือไฟล์ที่คัดลอกมาไม่ได้ลงท้ายด้วย .crt และ update-ca-certificates แจ้งว่า 0 added หรือเซิร์ฟเวอร์กำลังแสดงใบรับรองที่ต่างจากที่คุณเชื่อถือ ให้เปรียบเทียบ fingerprint ด้วย openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256
NET::ERR_CERT_COMMON_NAME_INVALID คือใบรับรองไม่มี SAN หรือ SAN ไม่ครอบคลุมชื่อที่อยู่ในแถบที่อยู่ กรณีคลาสสิกคือ SAN ระบุ DNS:git.internal.lan แต่ผู้ใช้เรียกดู https://10.8.0.1 การแก้ไข trust store ไม่สามารถแก้ปัญหานี้ได้ ให้ทำการออกใบรับรองใหม่โดยเพิ่มรายการที่ขาดหายไป
curl: (60) SSL certificate problem: self-signed certificate คือ curl ไม่เชื่อถือใบรับรองนั้น รูปแบบย่อย self-signed certificate in certificate chain หมายถึงสิ่งเดียวกันสำหรับใบรับรองที่ลงนามโดย private CA ของคุณ วิธีแก้ไขเฉพาะหน้าคือ curl --cacert lab-ca.crt https://... ส่วนวิธีแก้ไขถาวรคือการเพิ่มลงใน trust store ไม่ใช่ -k
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (หรือ Expecting: CERTIFICATE REQUEST หรือ no start line) คือความสับสนของรูปแบบ PEM คุณป้อนไฟล์ผิดประเภทให้กับ OpenSSL เช่น ป้อน key หรือ CSR ในที่ที่ควรจะเป็นใบรับรอง หรือป้อนไฟล์ไบนารี DER ในที่ที่ควรจะเป็น PEM โดย head -1 filename จะบอกคุณว่าไฟล์ที่คุณมีคืออะไร ใบรับรองที่ถูกต้องจะขึ้นต้นด้วย -----BEGIN CERTIFICATE----- สำหรับ DER ให้แปลงด้วย openssl x509 -inform der -in file.der -out file.crt
nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch) คือใบรับรองและ key ไม่ได้คู่กัน ซึ่งมักเกิดจากการรันคำสั่งสร้างใบรับรองสองครั้งแล้วไฟล์เกิดสลับกัน ให้ยืนยันด้วย openssl x509 -in git.internal.crt -noout -pubkey | sha256sum เทียบกับ openssl pkey -in git.internal.key -pubout | sha256sum หากค่า hash ตรงกันแสดงว่าเป็นคู่ที่ถูกต้อง หากค่าต่างกันให้สร้างใหม่ทั้งคู่พร้อมกัน
FAQ
ทำไม Chrome ยังแสดงสถานะ "Not secure" หลังจากที่ฉันสร้าง self-signed certificate ขึ้นมาเอง?
หากข้อผิดพลาดคือ NET::ERR_CERT_AUTHORITY_INVALID แสดงว่า certificate นั้นถูกต้องแล้ว เพียงแต่ Chrome ยังไม่มีเหตุผลที่จะเชื่อถือมัน ให้ติดตั้ง certificate ดังกล่าว (หรือ root certificate ของ CA ส่วนตัวของคุณ) ลงใน trust store ของไคลเอนต์ และโปรดจำไว้ว่าบน Linux นั้น Chrome จะใช้ฐานข้อมูล NSS ผ่าน certutil (ที่ตำแหน่ง ~/.local/share/pki/nssdb ตั้งแต่ Chrome 146 เป็นต้นไป หรือใช้ ~/.pki/nssdb แบบเดิมหากมีอยู่) ไม่ได้ใช้ trust store ของระบบ หากข้อผิดพลาดคือ NET::ERR_CERT_COMMON_NAME_INVALID แสดงว่า certificate ขาด Subject Alternative Name ที่ตรงกับ URL ซึ่งจะต้องออก certificate ใหม่โดยระบุ -addext "subjectAltName=..." ให้ถูกต้อง
ฉันจะทำให้ curl เชื่อถือ self-signed certificate โดยไม่ต้องใช้แฟล็ก -k ได้อย่างไร?
ให้คัดลอกไฟล์ certificate (รูปแบบ PEM, นามสกุล .crt) ไปไว้ที่ /usr/local/share/ca-certificates/ แล้วรันคำสั่ง sudo update-ca-certificates โดยผลลัพธ์จะต้องแสดงข้อความ 1 added จากนั้นเป็นต้นไป curl จะตรวจสอบ certificate ดังกล่าวเหมือนกับ certificate สาธารณะทั่วไป สำหรับการร้องขอแบบครั้งเดียวโดยไม่แก้ไขระบบ ให้ใช้ curl --cacert /path/to/cert.crt เพื่อตรวจสอบกับไฟล์นั้นโดยเฉพาะ ส่วน -k จะเป็นการปิดการตรวจสอบทั้งหมด ซึ่งไม่ควรนำไปใช้ในสคริปต์ใดๆ
self-signed certificate สามารถมีอายุการใช้งานได้นานเท่าใด?
ในทางเทคนิคคุณสามารถกำหนดอายุได้นานเท่าที่ต้องการ ข้อจำกัดของ CA/Browser Forum (ปัจจุบันคือ 200 วัน และจะเหลือ 47 วันภายในปี 2029) มีผลบังคับใช้เฉพาะกับ CA ที่ได้รับความเชื่อถือในระดับสาธารณะเท่านั้น ไม่รวมถึงความเชื่อถือภายในองค์กร ในทางปฏิบัติควรจำกัดอายุ certificate ของเซิร์ฟเวอร์ไว้ไม่เกิน 825 วัน เนื่องจากอุปกรณ์ของ Apple จะปฏิเสธ certificate ที่มีอายุเกินกว่านั้นไม่ว่าผู้ออกจะเป็นใครก็ตาม การกำหนดอายุ root certificate ส่วนตัวไว้ 10 ปี และ leaf certificate ไว้ 2 ปี (-days 730) เป็นค่าเริ่มต้นที่เหมาะสม เพียงแต่ต้องทำตารางแจ้งเตือนการต่ออายุไว้ เพราะหาก certificate ภายในหมดอายุ จะทำให้บริการทั้งหมดหยุดทำงานโดยไม่มีการแจ้งเตือนล่วงหน้าในวันที่ไม่มีใครคาดคิด
ฉันควรใช้ self-signed certificate หรือ Let's Encrypt?
หากบริการมีชื่อ DNS สาธารณะและสามารถเข้าถึงได้จากอินเทอร์เน็ต ให้ใช้ Let's Encrypt เสมอ เพราะฟรี เป็นอัตโนมัติ และได้รับความเชื่อถือจากเบราว์เซอร์และระบบปฏิบัติการส่วนใหญ่อยู่แล้ว (ตรวจสอบรายการความเข้ากันได้ของ Let's Encrypt หากจำเป็นต้องเชื่อมต่อกับอุปกรณ์รุ่นเก่ามาก) ส่วน self-signed (หรือ CA ส่วนตัว) เหมาะสำหรับกรณีที่ Let's Encrypt ไม่สามารถออกให้ได้ เช่น IP ภายใน, ชื่อโฮสต์ที่ใช้ได้เฉพาะภายในเครือข่ายอย่าง .lan, เครือข่ายที่ไม่มีการเชื่อมต่ออินเทอร์เน็ต (air-gapped) และบริการที่ตั้งใจซ่อนไว้หลัง VPN การตัดสินใจนี้ขึ้นอยู่กับการเข้าถึงและการตั้งชื่อ ไม่ใช่ความแข็งแกร่งของความปลอดภัย เนื่องจากเทคนิคการเข้ารหัสลับนั้นเหมือนกันทุกประการ
ทำไม certificate ของฉันจึงถูกปฏิเสธแม้ว่าจะเพิ่มลงใน /usr/local/share/ca-certificates แล้ว?
ให้ตรวจสอบ 3 ประเด็นดังนี้ ไฟล์จะต้องลงท้ายด้วย .crt หากใช้นามสกุล .pem ระบบจะข้ามไปโดยไม่แจ้งเตือนและ update-ca-certificates จะรายงานว่า 0 added เนื้อหาภายในต้องเป็นข้อความรูปแบบ PEM ที่ขึ้นต้นด้วย -----BEGIN CERTIFICATE----- ไม่ใช่รูปแบบไบนารี DER และแอปพลิเคชันนั้นต้องเรียกใช้ trust store ของระบบจริงๆ โดย Chrome บน Linux, Firefox, Python requests, Node.js (เว้นแต่จะเริ่มทำงานด้วย --use-system-ca) และ Java ต่างก็มี trust store ส่วนตัวที่ต้องเพิ่ม certificate เข้าไปแยกต่างหาก