SSD Nodes Learn
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-07-24

วิธีสร้าง Self-signed Certificate บน Ubuntu

สอนการใช้ openssl สร้างใบรับรอง TLS บน Ubuntu 24.04 ให้ Chrome ยอมรับด้วยการตั้งค่า SAN พร้อมวิธีตั้งค่า nginx และ Apache ให้ใช้งานได้โดยไม่ต้องใช้ curl -k

สิ่งที่คุณกำลังสร้าง

ใบรับรอง TLS แบบ self-signed ที่เบราว์เซอร์และ client สมัยใหม่ยอมรับ — มีการตั้งค่า subjectAltName ที่ถูกต้อง, มีการกำหนดสิทธิ์ key ที่เหมาะสม, และเชื่อมต่อกับ nginx หรือ Apache — รวมถึงส่วนที่คู่มือเกือบทุกฉบับข้ามไป นั่นคือการทำให้ client เชื่อถือ ใบรับรองอย่างถูกต้อง แทนที่จะต้องกดข้ามคำเตือนและต้องเขียน curl -k ลงใน script ตลอดไป เมื่อจบขั้นตอน คุณจะได้ private CA ที่ใช้เพียง 5 คำสั่ง สำหรับกรณีที่มีบริการภายในเพิ่มขึ้นจาก 1 เป็น 6 บริการ

เริ่มจากการตัดสินใจ เนื่องจากใบรับรองแบบ self-signed มีความจำเป็นน้อยกว่าที่คนส่วนใหญ่เข้าใจ หากบริการของคุณสามารถเข้าถึงได้จากอินเทอร์เน็ตสาธารณะผ่าน DNS name จริง ให้หยุดอ่านเนื้อหานี้แล้วไปใช้ a free Let's Encrypt certificate with certbot on nginx หรือ the Apache equivalent แทน วิธีนี้ไม่มีค่าใช้จ่าย, มีการต่ออายุอัตโนมัติ, และเบราว์เซอร์ทุกตัวทั่วโลกให้ความเชื่อถืออยู่แล้ว การใช้ self-signed certificate บนเว็บไซต์สาธารณะจะทำให้ผู้ใช้งานคุ้นชินกับการกดข้ามคำเตือนด้านความปลอดภัย ซึ่งเป็นพฤติกรรมที่แย่กว่าการใช้ HTTP ปกติ

Self-signed เป็นเครื่องมือที่เหมาะสมเมื่อไม่มีอินเทอร์เน็ตสาธารณะเข้ามาเกี่ยวข้อง เช่น แผงควบคุม admin ที่ผูกกับ a WireGuard tunnel address on your VPS, เครื่อง staging ในเครือข่ายส่วนตัว, การรับส่งข้อมูลระหว่าง backend, อุปกรณ์ใน home-lab, หรือการใช้แทนใบรับรองเริ่มต้นที่ Webmin generates for itself on port 10000 ทั้งนี้ Let's Encrypt ไม่สามารถออกใบรับรองสำหรับ 10.8.0.1 หรือ git.internal.lan ได้ เนื่องจากไม่มี public CA รายใดจะออกใบรับรองที่มี private IP หรือ TLD ที่สร้างขึ้นเอง สำหรับชื่อเหล่านี้ คุณต้องทำหน้าที่เป็น CA เอง

เนื้อหาทั้งหมดด้านล่างนี้รันบน Ubuntu 24.04 เครื่องใหม่ ซึ่งมาพร้อมกับ OpenSSL 3.0.x (ใช้ openssl version เพื่อยืนยัน) ทุกขั้นตอนไม่จำเป็นต้องใช้การเชื่อมต่ออินเทอร์เน็ต สามารถทำงานแบบ air-gapped ได้

สาเหตุที่คำสั่งบรรทัดเดียวแบบเดิมทำให้ Chrome ปฏิเสธ certificate

คำสั่งที่พบในบทเรียนก่อนปี 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

คำสั่งนี้จะถามคำถามแบบ interactive หลายข้อ โดยจะใส่ hostname ของคุณลงในฟิลด์ Common Name และสร้าง certificate ที่ไม่มีส่วนขยาย subjectAltName Certificate ดังกล่าวไม่สามารถใช้งานได้ เนื่องจาก Chrome เลิกอ่านค่าจาก Common Name ตั้งแต่เวอร์ชัน 58 เมื่อเดือนเมษายน 2017 (ซึ่ง RFC 2818 ได้ยกเลิกการใช้ CN matching มาตั้งแต่ปี 2000 แล้ว) และ Firefox, Safari, curl และ Python ก็ทำงานในลักษณะเดียวกัน Certificate จะระบุตัวตนของ server ผ่านส่วนขยาย SAN เท่านั้น มิเช่นนั้นจะไม่สามารถระบุได้ และ browser จะแจ้งข้อความดังนี้:

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 ไม่สามารถแก้ปัญหานี้ได้ เนื่องจาก certificate ไม่มีการระบุชื่อใดๆ หากคุณพบข้อความ NET::ERR_CERT_COMMON_NAME_INVALID แสดงว่า certificate ของคุณไม่มี SAN (หรือใช้ SAN ที่ไม่ถูกต้อง) และคุณจำเป็นต้องสร้าง certificate ใบใหม่ โชคดีที่การแก้ไขใช้เพียงคำสั่งเดียวเท่านั้น

การสร้าง certificate ที่ browser ยอมรับด้วยคำสั่งเดียว

OpenSSL เพิ่ม flag -addext ในเวอร์ชัน 1.1.1 ซึ่งทำให้คุณไม่จำเป็นต้องตั้งค่า config-file ที่ซับซ้อนเหมือนในคู่มือเก่าเพื่อใส่ 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"

รายละเอียดของแต่ละ flag:

  • -x509 ใช้สำหรับสร้าง self-signed certificate โดยตรงแทนการสร้าง signing request
  • -newkey rsa:4096 ใช้สร้าง key ใหม่ในขั้นตอนเดียวกัน การใช้ RSA 4096 สามารถใช้งานกับ client รุ่นเก่าได้ทุกรุ่น แต่หากอุปกรณ์ที่เชื่อมต่อทั้งหมดเป็นรุ่นใหม่ การใช้ -newkey ec -pkeyopt ec_paramgen_curve:P-256 จะมีขนาดเล็กกว่าและทำงานได้เร็วกว่า
  • -noenc คือรูปแบบการเขียนของ OpenSSL 3.x ซึ่งแทนที่ -nodes แบบเดิม โดยจะไม่มีการตั้ง passphrase ใน key การใช้ key ที่มี passphrase จะทำให้ nginx หยุดรอการป้อนข้อมูลทุกครั้งที่เริ่มระบบ ดังนั้นสำหรับการใช้งานบน server ควรใช้รูปแบบนี้
  • -days 730 คือระยะเวลา 2 ปี รายละเอียดเพิ่มเติมเกี่ยวกับตัวเลขนี้อยู่ในส่วนการหมดอายุ
  • -subj ใช้สำหรับตอบคำถามแบบ interactive โดยอัตโนมัติ ปัจจุบันค่า CN มีไว้เพื่อความสวยงามเท่านั้น แต่ควรตั้งค่าเป็นชื่อหลักไว้ เนื่องจากเครื่องมือบางอย่างยังมีการแสดงผลค่านี้อยู่
  • -addext "subjectAltName=..." คือ flag ที่สำคัญที่สุด ให้ระบุ ทุก ชื่อและ ทุก IP ที่ client จะใช้งาน โดยใช้ DNS: สำหรับ hostname (สามารถใช้ wildcard เช่น DNS:*.internal.lan ได้) และใช้ IP: สำหรับ address หากมีการเข้าใช้งานผ่าน https://10.8.0.1 จะต้องมีรายการ IP:10.8.0.1 อยู่ในนั้น หากใช้ SAN เฉพาะ DNS จะทำให้เกิดปัญหา 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 แสดงว่า certificate ไม่มี SAN และ browser จะปฏิเสธการเชื่อมต่อ ให้ทำการสร้างใหม่แทนการดำเนินการต่อ

Lock down the key

A private key readable by every user on the box is not a private key. On Ubuntu, /etc/ssl/private is already 710 root:ssl-cert, which keeps casual eyes out, but set the file itself explicitly:

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

nginx and Apache both read certificates as root before dropping privileges, so root:root mode 600 works for them. If the key is for a service that runs as its own user and loads the key itself — a Node app, Gitea, a Python daemon — chown it to that service user instead, still mode 600. What you never do: mode 644, a copy in a git repository, or a copy in /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 nginx

nginx -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_http

ssl เพียงอย่างเดียวไม่เพียงพอสำหรับกรณีนี้: 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 apache2

configtest ควรจะตอบกลับ Syntax OK ให้ทดสอบจากเครื่อง client:

curl -v https://git.internal.lan/

และคุณจะพบข้อผิดพลาด:

curl: (60) SSL certificate problem: self-signed certificate

นี่ไม่ใช่ bug แต่เป็นลักษณะการทำงานของ TLS: curl ไม่รู้จัก certificate ของคุณ จึงปฏิเสธการเชื่อมต่อกับ server ที่ไม่สามารถตรวจสอบความถูกต้องได้ ส่วนถัดไปคือวิธีการแก้ไขที่ถูกต้อง ซึ่งไม่ใช่สิ่งที่คนส่วนใหญ่บนอินเทอร์เน็ตกำลังทำอยู่ในขณะนี้

วิธีทำให้ client เชื่อถือใบรับรอง — และรูปแบบที่ไม่ควรทำ (anti-patterns) ที่ควรหลีกเลี่ยง

วิธีแก้ไขที่ผิดซึ่งควรระบุให้ชัดเจน ได้แก่ การใส่ curl -k (หรือ --insecure) ไว้ใน script, การใช้ verify=False ใน Python requests หรือ NODE_TLS_REJECT_UNAUTHORIZED=0 ใน Node — วิธีเหล่านี้ไม่สามารถทำให้ certificate ของคุณได้รับความเชื่อถือได้ วิธีเหล่านี้เป็นการปิด (off) การตรวจสอบ certificate ซึ่งหมายความว่า client จะยอมเชื่อมต่อกับ server ใดก็ได้ที่ส่ง certificate ใดๆ มาให้ รวมถึง certificate ที่ผู้โจมตีปลอมแปลงขึ้นมาด้วย คุณยังต้องเสียทรัพยากรในการทำ TLS แต่กลับสูญเสียการยืนยันตัวตนซึ่งเป็นวัตถุประสงค์หลักไป ยิ่งไปกว่านั้น flag เหล่านี้มักจะแพร่กระจายไปทั่ว เช่น ถูกคัดลอกไปวางใน cron job, ใน deploy script และใน production code จนไม่มีใครจำได้ว่าการเชื่อมต่อใดที่ตั้งใจให้เป็นแบบชั่วคราว หาก verify=False ยังคงอยู่หลังจากเสร็จสิ้นขั้นตอนการ debug แสดงว่าการออกแบบระบบนั้นผิดพลาด

วิธีแก้ไขที่ถูกต้องคือการทำให้ OS ของแต่ละ client รับรู้ว่า certificate นี้คือ trusted root สำหรับ Ubuntu และ Debian client:

sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates

บรรทัดที่สำคัญในผลลัพธ์ (ตามด้วย block Running hooks in /etc/ca-certificates/update.d...):

Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

มีข้อควรระวังสองประการในบรรทัดเหล่านี้ ไฟล์ ต้อง ลงท้ายด้วย .crt — หากใช้ extension แบบ .pem ระบบจะข้ามไปโดยไม่แจ้งเตือนและคุณจะพบ 0 added โดยไม่มี error message และเนื้อหาในไฟล์ต้องเป็นรูปแบบ PEM โดยไฟล์ต้องเริ่มต้นด้วย -----BEGIN CERTIFICATE----- หากคุณมีไฟล์ binary แบบ DER ให้แปลงก่อนด้วย openssl x509 -inform der -in file.der -out file.crt การเพิ่ม self-signed certificate เข้าไปเป็น root โดยตรงสามารถทำได้เพราะ self-signed certificate คือ root ในตัวมันเอง

หลังจากนั้น curl, wget, git, apt และเครื่องมืออื่นๆ ที่ใช้ OpenSSL ร่วมกับ system bundle จะเชื่อถือ server โดยไม่ต้องใช้ flag ใดๆ ทั้งสิ้น อย่างไรก็ตาม client บางตัวมี trust store เป็นของตัวเองและต้องจัดการแยกต่างหาก:

  • Chrome/Chromium บน Linux จะอ่านจาก NSS database ไม่ใช่ system store: ใช้ sudo apt install libnss3-tools แล้วตามด้วย certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt ต่อหนึ่งผู้ใช้งาน
  • Firefox มี store ของตัวเอง: ไปที่ Settings → Privacy & Security → Certificates → Import หรือเปลี่ยน security.enterprise_roots.enabled เป็น true ใน about:config เพื่อให้อ่านจาก system store
  • 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: ให้ export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt

สำหรับ Windows client ให้ดับเบิลคลิกที่ .crt และติดตั้งลงใน Trusted Root Certification Authorities; สำหรับ macOS ให้เพิ่มลงใน System keychain ใน Keychain Access และตั้งค่าเป็น Always Trust

One root for many services: a small private CA

การสร้างความเชื่อถือ (trust) แยกตามแต่ละใบรับรองทำให้การขยายระบบทำได้ยาก: หากมี 6 services และ 4 client machines จะต้องติดตั้ง trust ถึง 24 ครั้ง และจำนวนนี้จะเพิ่มขึ้นตามจำนวน service ใหม่ วิธีแก้ไขคือการใช้ private CA — โดยให้ client เชื่อถือ root เพียง one ตัว และใช้ root นั้นในการ sign ใบรับรองของแต่ละ service

ตัวเลือกที่ใช้งานง่ายคือ mkcert ซึ่งมีอยู่ใน Ubuntu 24.04 repos และสามารถจัดการ NSS stores (Chrome, Firefox) ซึ่ง update-ca-certificates ไม่สามารถทำได้:

sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1

mkcert -install จะสร้าง root และลงทะเบียนไว้ในทุก trust store บนเครื่องนั้น; คำสั่งที่สามจะสร้าง git.internal.lan+2.pem และ git.internal.lan+2-key.pem ซึ่งสามารถนำไปใช้ใน nginx หรือ Apache snippets ด้านบนได้ทันที การออกแบบนี้เหมาะสำหรับเครื่อง development — เนื่องจาก root key จะถูกเก็บไว้ในเครื่องที่รัน -install — ดังนั้นจึงเหมาะสำหรับ dev laptop แต่ไม่เหมาะสำหรับ server fleet

สำหรับ server การใช้ 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 โดย default รวมถึง SAN ที่คุณตั้งใจเพิ่มเข้าไปด้วย การใช้ -copy_extensions copy (ซึ่งเป็น option ของ OpenSSL 3.x จึงใช้งานได้บน 24.04) จะช่วยรักษา extensions เหล่านั้นไว้; หากไม่ใช้ option นี้ ใบรับรองที่ถูก sign จะไม่มี SAN และ Chrome จะแสดงข้อความ NET::ERR_CERT_COMMON_NAME_INVALID อีกครั้ง ตรวจสอบความถูกต้องด้วยการเช็คแบบ openssl x509 -noout -ext subjectAltName เช่นเดิม

แจกจ่าย lab-ca.crt ให้กับ client ผ่านขั้นตอนการจัดการ trust-store ด้านบน — ทำเพียงครั้งเดียวต่อหนึ่งเครื่อง ตลอดไป และต้องรักษาความปลอดภัยให้ lab-ca.key อย่างเข้มงวด: กำหนด mode เป็น 600 และควรเก็บไว้ในเครื่องที่ไม่ใช่หนึ่งใน server ที่ถูก sign เนื่องจากผู้ที่ถือไฟล์นี้สามารถสร้างใบรับรองสำหรับชื่อใดก็ได้ที่ client จะเชื่อถือ

Expiry and rotation

อายุการใช้งานของ Public CA certificate กำลังลดลง — CA/Browser Forum ได้กำหนดเพดานสูงสุดสำหรับ certificate ที่ออกโดย Public CA ไว้ที่ 200 วัน ในเดือน March 2026 (ลดลงจาก 398 วัน) และจะลดลงเหลือ 100 วันในปี 2027 และ 47 วันภายในเดือน March 2029 อย่างไรก็ตาม กฎเหล่านี้ใช้บังคับเฉพาะ CA ที่เป็น publicly trusted เท่านั้น Private CA ของคุณไม่อยู่ภายใต้กฎนี้ และ browser ไม่ได้บังคับใช้กฎดังกล่าวกับ root ที่ติดตั้งด้วยตนเอง มีข้อจำกัดหนึ่งที่ใช้งานจริงคือ: Apple platforms จะปฏิเสธ TLS server certificate ใดก็ตามที่มีอายุการใช้งานนานกว่า 825 วัน ไม่ว่าจะออกโดยใครก็ตาม ดังนั้นหากต้องการให้ iPhone หรือ Mac เชื่อมต่อได้ ควรตั้งค่า leaf certificates ให้มีอายุไม่เกิน 2 ปี -days 730 สามารถผ่านเกณฑ์นี้ได้ในทุกระบบ การใช้ root อายุ 10 ปี ร่วมกับ leaf อายุ 2 ปี เป็นโครงสร้างภายในที่เหมาะสม

Certificate ที่มีอายุการใช้งานยาวนานจะเกิดปัญหาในรูปแบบเดียวคือ: ระบบจะล้มเหลวพร้อมกันโดยไม่มีการแจ้งเตือนล่วงหน้า ในวันที่ไม่มีใครจำได้ว่าเลือกไว้เมื่อใด ตรวจสอบข้อมูลที่คุณมี:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

ควรบันทึกการต่ออายุลงในปฏิทิน หรือใช้ cron เพื่อแจ้งเตือนล่วงหน้า 30 วัน — openssl x509 -checkend 2592000 -in cert.crt จะส่งค่า exit non-zero เมื่อเหลือเวลาอีก 30 วันก่อนหมดอายุ หากคุณใช้งาน Uptime Kuma for status monitoring อยู่แล้ว ฟีเจอร์ HTTPS monitors สามารถแจ้งเตือนเมื่อ certificate ใกล้หมดอายุได้โดยไม่มีค่าใช้จ่าย

การทำ Rotation ด้วย Private CA นั้นทำได้ง่ายและไม่ซับซ้อน: เพียงรันคำสั่ง CSR-and-sign ใหม่ เปลี่ยนไฟล์ และ reload web server เนื่องจาก root ไม่มีการเปลี่ยนแปลง จึงไม่มี client รายใดสังเกตเห็นความผิดปกติ

Failure modes, with the strings you will see

NET::ERR_CERT_AUTHORITY_INVALID — คือสถานะที่ คาดการณ์ไว้ ก่อนการติดตั้ง trust ไม่ใช่ข้อผิดพลาดของ certificate หากปัญหายังคงอยู่ หลังจาก ติดตั้ง root แล้ว: สำหรับ Linux นั้น Chrome จะอ่านค่าจาก NSS แทนที่จะเป็น system store (ดูขั้นตอน certutil); หรือไฟล์ที่คัดลอกมาไม่มีส่วนท้ายเป็น .crt และ update-ca-certificates ระบุว่า 0 added; หรือ server ส่ง certificate ตัวอื่นที่ไม่ใช่ตัวที่คุณติดตั้ง trust ไว้ — ให้ตรวจสอบ 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 — certificate ไม่มีค่า SAN หรือค่า SAN ไม่ครอบคลุมชื่อที่ปรากฏใน address bar กรณีทั่วไปคือ: SAN ระบุค่า DNS:git.internal.lan แต่ผู้ใช้เข้าใช้งานผ่าน https://10.8.0.1 การแก้ไขใน trust-store ไม่สามารถแก้ปัญหานี้ได้ ต้องทำการออก certificate ใหม่โดยเพิ่ม entry ที่ขาดหายไป

curl: (60) SSL certificate problem: self-signed certificate — curl ไม่เชื่อถือ certificate กรณี self-signed certificate in certificate chain มีความหมายเดียวกันสำหรับ certificate ที่ลงนามโดย 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 ในขณะที่ระบบต้องการ certificate หรือใช้ไฟล์ binary แบบ DER ในขณะที่ระบบต้องการ PEM โดย head -1 filename จะระบุว่าไฟล์ที่คุณมีคืออะไร — certificate จะเริ่มต้นด้วย -----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) — certificate และ 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 ยังไม่มีเหตุผลที่จะเชื่อถือ (trust) certificate ดังกล่าว ให้ติดตั้ง certificate (หรือ private CA root) ลงใน trust store ของเครื่อง client และโปรดทราบว่าบน Linux นั้น Chrome จะใช้ NSS database ผ่านทาง certutil แทนที่จะใช้ system store หากพบข้อผิดพลาด NET::ERR_CERT_COMMON_NAME_INVALID แสดงว่า certificate ขาด Subject Alternative Name ที่ตรงกับ URL และต้องทำการออก certificate ใหม่ด้วย -addext "subjectAltName=..."

ฉันจะทำให้ curl เชื่อถือ self-signed certificate โดยไม่ใช้ flag -k ได้อย่างไร?

คัดลอก certificate (รูปแบบ PEM, นามสกุล .crt) ไปไว้ใน /usr/local/share/ca-certificates/ แล้วรันคำสั่ง sudo update-ca-certificates — ผลลัพธ์ต้องแสดงข้อความ 1 added หลังจากนั้น curl จะตรวจสอบความถูกต้องเหมือนกับ certificate สาธารณะทั่วไป หากต้องการเรียกใช้งานเพียงครั้งเดียวโดยไม่แก้ไขระบบ ให้ใช้ curl --cacert /path/to/cert.crt เพื่อตรวจสอบกับไฟล์นั้นโดยเฉพาะ ส่วน -k จะเป็นการปิดการตรวจสอบความถูกต้องทั้งหมด ซึ่งไม่ควรนำไปใช้ใน script ใดๆ

self-signed certificate สามารถมีอายุการใช้งานได้นานเท่าใด?

ในทางเทคนิคสามารถกำหนดได้นานเท่าที่ต้องการ เนื่องจากข้อจำกัดของ CA/Browser Forum (ปัจจุบันคือ 200 วัน และจะเหลือ 47 วันภายในปี 2029) ใช้บังคับกับ CA ที่ได้รับความเชื่อถือในระดับสาธารณะเท่านั้น ไม่ได้ใช้กับ private trust ในทางปฏิบัติ ควรจำกัดอายุของ server certificates ไว้ที่ 825 วัน เนื่องจากอุปกรณ์ของ Apple จะปฏิเสธ certificate ที่มีอายุยาวกว่านั้นไม่ว่าจะออกโดยผู้ออกใบรับรองใดก็ตาม การใช้ private root อายุ 10 ปี ร่วมกับ leaf certificates อายุ 2 ปี (-days 730) เป็นค่าเริ่มต้นที่เหมาะสม ควรวางแผนการต่ออายุล่วงหน้า เพราะหาก certificate ภายในหมดอายุ ระบบทั้งหมดจะหยุดทำงานโดยไม่มีการแจ้งเตือนล่วงหน้า

ฉันควรใช้ self-signed certificate หรือ Let's Encrypt?

หากบริการนั้นมี public DNS name และสามารถเข้าถึงได้จากอินเทอร์เน็ต ให้ใช้ Let's Encrypt เสมอ เนื่องจากใช้งานฟรี ทำงานอัตโนมัติ และได้รับความเชื่อถือจาก client ทุกราย ส่วน self-signed (หรือ private CA) ใช้สำหรับกรณีที่ Let's Encrypt ไม่สามารถออกให้ได้ เช่น private IPs, hostname ภายในอย่าง .lan, เครือข่ายแบบ air-gapped และบริการที่ตั้งใจซ่อนไว้หลัง VPN การตัดสินใจขึ้นอยู่กับความสามารถในการเข้าถึงและการตั้งชื่อ ไม่ใช่ความแข็งแกร่งของการเข้ารหัส เนื่องจากหลักการทางวิทยาการรหัสลับนั้นเหมือนกัน

ทำไม certificate ของฉันถึงถูกปฏิเสธ แม้ว่าจะเพิ่มลงใน /usr/local/share/ca-certificates แล้ว?

ตรวจสอบ 3 ประเด็นดังนี้ ไฟล์ต้องลงท้ายด้วย .crt หากเป็นนามสกุล .pem ระบบจะข้ามไฟล์นั้นไปโดยไม่แจ้งเตือน และ update-ca-certificates จะรายงานข้อผิดพลาด 0 added เนื้อหาในไฟล์ต้องเป็นข้อความรูปแบบ PEM ที่เริ่มต้นด้วย -----BEGIN CERTIFICATE----- ไม่ใช่ไฟล์ binary แบบ DER และแอปพลิเคชันต้องเรียกใช้ system store จริงๆ เนื่องจาก Chrome บน Linux, Firefox, Python requests, Node.js และ Java ต่างก็มี trust store ส่วนตัว และจำเป็นต้องเพิ่ม certificate แยกต่างหาก

#openssl#tls#self-signed#ubuntu#security