ใส่ SSL ให้เว็บบน nginx กับ Let’s Encrypt

เมื่อเช้าเพิ่งได้ close beta invite ของ Let’s Encrypt มาครับ ขอไปหลายโดเมนก็ได้พร้อมกันชุดเดียวเลย

ทีนี้ได้มาจะทำอะไรบ้าง ผมก็ต้องเล่า setup ผมก่อนละกัน สำหรับบล็อคที่กำลังอ่านอยู่นี่จะใช้ web server 2 ตัวทำงานคู่กันครับ คือมี nginx เป็น reverse proxy แล้วเข้ามาที่ apache อีกทีนึง ซึ่งก็แน่นอนว่าผมต้องเข้าไปเซต nginx ที่มันทำหน้าที่ SSL termination อยู่ (อธิบายง่ายๆ คือถึงจะเข้าเว็บมาเป็น https แต่การเชื่อมต่อระหว่าง nginx -> apache ผมไม่เข้ารหัสนะครับ)

ในอีเมลก็จะมีแจ้งข้อความมาว่าสามารถปรับเป็น production server ได้อย่างไร

git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
./letsencrypt-auto --server \
     https://acme-v01.api.letsencrypt.org/directory --help

ก็จะสังเกตว่าต้องระบุ --server https://acme-v01.api.letsencrypt.org/directory เสมอนะครับเวลาใช้งาน ทีนี้ผมก็รำคาญที่ letsencrypt-auto มันพยายาม auto update ตลอดเวลาทุกครั้งที่รัน ก็เลยหาวิธีรันแบบไม่ต้องอัพเดต ซึ่งก็พบว่าไฟล์โปรแกรมอยู่ที่ ~/.local/share/letsencrypt/bin/letsencrypt สามารถกดรันได้เลย

ขอ Certificate ด้วยวิธี webroot

ต้องบอกก่อนว่า Let’s Encrypt มีวิธีที่มันจะ auto setup ทั้งระบบให้เลย แต่สำหรับใน beta นี้ไม่มีให้ใช้กับ nginx ครับ ฉะนั้นก็ต้องลงมือกันหน่อยละ

เวลาจะขอ Certificate จากผู้รับรอง SSL ไม่ว่าเจ้าไหนๆ ก็จะต้องมีการขอตรวจสอบกันก่อนว่าเป็นเจ้าของเว็บตัวจริงมั้ย ซึ่งปกติแล้วก็จะใช้วิธีส่งเมลมาที่ webmaster@example.com แต่สำหรับ Let’s Encrypt จะใช้วิธีที่ automate กันง่ายขึ้น คล้ายๆ กับการยึนยันเว็บใน Google Webmaster Tools ครับ นั่นคือการเอาไฟล์ไปแสดงที่ๆ ตกลงกันไว้ หรือจะใช้วิธีอื่นๆ ก็มีให้เลือกหลายวิธีเหมือนกัน

วิธีการใช้งานก็ไม่ยากครับ เพียงแค่สั่งดังนี้

sudo ~/.local/share/letsencrypt/bin/letsencrypt certonly -a webroot --server https://acme-v01.api.letsencrypt.org/directory --agree-dev-preview -d blog.whs.in.th --webroot-path /var/www/virtual/whs.in.th/blog/htdocs

ก็จะเห็นว่าตัวคำสั่งจะต้องระบุชื่อเว็บไซต์ที่ต้องการขอด้วยนะครับ และต้องระบุพาธไปยัง directory ของเว็บด้วย โดยโปรแกรมจะเอาไฟล์ไปใส่ไว้ในที่ๆ ถูกต้องเองและ server จะต้องอนุญาตให้เข้าไฟล์นั้นจากเว็บได้ จากนั้นก็จะปรากฏข้อความว่าขอ certificate เรียบร้อย

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/blog.whs.in.th/fullchain.pem. Your cert will
   expire on 2016-02-22. To obtain a new version of the certificate in
   the future, simply run Let's Encrypt again.

ก็จะได้ไฟล์ Certificate มาแล้วครับ ซึ่งถ้าเราดูใน /etc/letsencrypt/live/blog.whs.in.th/ จะประกอบด้วยไฟล์ 4 ไฟล์ดังนี้

  • cert.pem เก็บใบรับรองของ blog.whs.in.th เท่านั้น
  • chain.pem เก็บใบรับรอง Let’s Encrypt Authority X1 เข้าไปโหลดในเว็บได้เหมือนกัน
  • fullchain.pem ก็คือไฟล์ chain.pem + cert.pem ต่อกัน
  • privkey.pem เป็นกุญแจส่วนตัว อันนี้ก็อย่าเอาไปแจกจ่ายใคร

ซึ่งเวลาเรา setup server เราจะต้องใช้ไฟล์ fullchain ในการ setup นะครับ เนื่องจากการตรวจสอบของ browser จะใช้กระบวนการดังนี้

  1. มองหาว่าใครเป็นผู้รับรอง blog.whs.in.th นั่นคือ Let’s Encrypt Authority X1
  2. เนื่องจากใบรับรอง Let’s Encrypt Authority X1 ไม่ได้ลงทะเบียนกับ browser ไว้จึงจะหาใบรับรองต่อ
  3. Let’s Encrypt Authority X1 ถูกเซ็นด้วย DST Root CA X3 อีกที ซึ่งตรงนี้ browser พบว่ามีเก็บไว้ในเครื่อง (ติดตั้งมาพร้อมกับ OS หรือ browser แล้ว) ก็จะตรวจสอบว่าเค้ายึนยันจริงมั้ย ขั้นตอนนี้ทำให้ browser ยมอรับ Let’s Encrypt Authority X1
  4. browser ก็จะตรวจสอบต่อมาว่า Let’s Encrypt Authority X1 รับรอง blog.whs.in.th มั้ย ถ้าใช่ก็ถือว่าเว็บนี้ผ่าน

ถ้าเราใช้แค่ cert.pem ตัว browser จะไม่มีกุญแจสาธารณะของ Let’s Encrypt X1 จึงไม่สามารถยึนยันได้ว่าเว็บของเราถูกรับรองจริงมั้ย เพราะมันไม่ได้เซฟมากับเครื่อง ตัว server ต้องส่งมาให้

สำหรับการเอาใบรับรองใช้ใน nginx ก็ไม่ยุ่งยากครับ แก้ config file ดังนี้ (เช่นที่ /etc/nginx/sites-enabled/default)

server {
    listen :443 ssl;
    # ....
    ssl_certificate /etc/letsencrypt/live/blog.whs.in.th/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.whs.in.th/privkey.pem;
}

restart server ก็เป็นอันเสร็จ!

ขอ Certificate ด้วยวิธี standalone

Let’s Encrypt ไม่รองรับการขอ wildcard ครับ แต่ใบรับรอง 1 ใบสามารถมี Subject Alternate Name หลายๆ ชื่อได้ พูดง่ายๆ คือสามารถรับรองหลายๆ เว็บพร้อมกันได้เลยในใบเดียว ซึ่งกระบวนการตรวจสอบก็จะเหมือนกันครับคือจะเข้ามาเช็ค URL ที่กำหนดให้ในทุกๆ โดเมนที่ให้มา

ปัญหาก็คือ webroot มันเอาไฟล์ไปไว้ที่ document ของเว็บเดียว แล้วจะให้มันรับรองทุกเว็บได้อย่างไร… หรือถ้าเว็บไม่มี web server แต่จะเอาใบรับรองไปใช้กับ service อื่นๆ จะทำอย่างไร

คำตอบก็ไม่ยากครับ ให้ Let’s Encrypt เป็น web server เองเลยสิ!

วิธีการใน beta ค่อนข้างจะลำบากหน่อยครับ ก่อนอื่นต้องปิด web server ให้หมดก่อน

sudo /etc/init.d/apache2 stop
sudo /etc/init.d/nginx stop

ข้อสังเกตคือ ต่อให้ web server นั้นไม่ได้ bind port 80 ใน public IP ของเราก็จำเป็นจะต้องหยุดก่อนนะครับ ทุก interface จะต้องไม่มี port 80 ใช้เลย (#1515)

หลังจากนั้นแล้วก็สามารถขอใบรับรองได้เลย

sudo ~/.local/share/letsencrypt/bin/letsencrypt certonly -a standalone -d madoka.whs.in.th -d www.whs.in.th -d whs.in.th --server https://acme-v01.api.letsencrypt.org/directory --agree-dev-preview

ตัว Let’s Encrypt ก็จะเปิด web server มาเทสให้ครับ หลังจากนั้นแล้วก็จะได้ใบรับรองมา วิธีเซต server ก็ทำได้เช่นเดียวกันเลย (ใบรับรองจะเก็บอยู่ที่ folder ตามโดเมนแรกที่ระบุเข้าไปนะครับ)

Secure SSL ให้ปลอดภัย

ทีนี้ SSL ในปัจจุบันก็มีช่องโหว่มากมาย เวลาเซตก็ควรจะปิดพวก settings ต่างๆ ที่ทำให้สามารถ crack การเชื่อมต่อได้ เดิมทีผมใช้ของ CloudFlare แต่ปัจจุบันผมก็เห็นว่ามี Guide ของ Mozilla มาเพิ่มเติม

เอาแบบสรุปเลยนะครับ ในตรงที่เราใส่ ssl_certificate ใน nginx config เมื่อกี้ (หรือจะไว้ใน http block ก็ได้ ไม่ผิดกฎแต่อย่างใด) ใส่คำสั่งดังนี้ครับ

ssl_session_timeout 5m;
ssl_session_cache shared:SSL:5m;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/letsencrypt_ocsp_chain.pem;

สำหรับตรง ssl_ciphers สามารถเลือกจากในเว็บ Mozilla ได้เลยครับ อันนี้คือแบบ Modern ซึ่งแปลว่ามีความปลอดภัยสูง รองรับ Firefox 27, Chrome 22, IE 11, Opera 14, Safari 7 ขึ้นไป แต่ถ้าจะเอาตั้งแต่ยุคแบบ Firefox 1 Chrome 1 หรือถอยไปยัน IE6 ก็ไปเลือกในเว็บได้

สองจุดที่จะแนะนำคือคำสั่ง ssl_dhparam และคำสั่ง ssl_stapling ครับ ที่จะต้องเอาไฟล์ไปใส่ด้วย

dhparam

SSL ในยุคใหม่มีคนคิดค้นเทคนิคที่ชื่อว่า Forward Secrecy ขึ้นมาครับ อธิบายง่ายๆ ก็คือกุญแจแบบใช้แล้วทิ้ง ในอดีตเมื่อเราเชื่อมต่อกับ server ก็จะใช้กุญแจของ server ทำงานเลย ปัญหาที่ตามมาคือเมื่อกุญแจส่วนตัวของ server ถูกขโมย ข้อมูลที่ถูกดักฟังไว้ในอดีตจะสามารถแกะได้ทั้งหมด แต่หากใช้เทคนิคนี้เมื่อเชื่อมต่อ ทั้งสองฝ่ายจะสร้างกุญแจอันใหม่ขึ้นมาแล้วเซ็นด้วยกุญแจของ server อีกที เมื่อเชื่อมต่อเสร็จแล้วก็จะทำลายกุญแจส่วนตัวที่ใช้เชื่อมต่อทิ้ง ทำให้ไม่สามารถแกะข้อมูลย้อนหลังได้อีกต่อไป

กระบวนการที่ server ส่งกุญแจให้ client จะส่งจำนวนเฉพาะจำนวนหนึ่งไปด้วย ซึ่งตัว OpenSSL เองก็จะมีตัวเลขนี้เก็บไว้อยู่แล้ว แต่อาจจะมีความไม่ปลอดภัย หรือเล็กเกินไป (จำนวนเฉพาะที่ใช้ควรมีขนาดใหญ่อย่างน้อยเท่ากับใบรับรอง เช่นถ้าใช้ใบรับรอง 4096 bit ก็ต้องสร้างจำนวนขนาด 4096 บิตมาด้วย) ฉะนั้นแล้วควรจะสร้างขึ้นมาใหม่

วิธีสร้างก็ไม่ยากครับ

sudo openssl dhparam 2048 -out /etc/nginx/dhparam.pem

อดใจรอครู่เดียวก็ได้แล้ว ทั้งนี้มีข้อแม้คือถ้า client ที่ใช้เว็บของเรารวมถึงโปรแกรมที่รันบน Java 6 จะเชื่อมต่อกับ server ที่ใช้ dhparam เกิน 1024 บิตไม่ได้นะครับ

ssl_stapling

ปัญหาต่อมาในการเชื่อมต่อก็คือ จะรู้ได้ยังไงว่าใบรับรองที่ใช้งา่นอยู่ยังไม่ถูกระงับ ในระบบยุคเดิมจะใช้ระบบ CSR ก็คือมีไฟล์บัญชี blacklist เลยว่าใบรับรองใบใดบ้างระงับแล้ว ปัญหาก็คือ ไฟล์มันใหญ่มาก ก็เลยไม่มีใครดู

ปัจจุบันก็เลยมีเทคนิคใหม่ขึ้นมาครับเรียกว่า OCSP ซึ่งวิธีก็คือเวลาผมจะเข้า blog.whs.in.th ในใบรับรองก็จะบอกว่าให้เช็คกับ http://ocsp.int-x1.letsencrypt.org/ ได้ว่ามีการระงับมั้ย ก็จะสะดวกกว่าเพราะถามทีละใบ

แต่ปัญหาก็คือ… มันทำให้เข้าเว็บช้าลง เพราะต้องถาม 2 server ตลอดเวลา

ก็มีเทคนิคเข้ามาแก้ปัญหานี้ครับคือ OCSP Stapling นั่นคือก็ให้ server ผมไปถามเว็บ ocsp แล้วจำคำตอบไว้เลย เวลาใครเข้าเว็บผมมาก็ก๊อปคำตอบส่งให้ได้เลย

สำหรับการตั้งค่า ssl_stapling นี่ก็คือการให้ nginx เตรียมคำตอบ OCSP ไว้ล่างหน้าเลยครับ ซึ่งจะใช้ประกอบกับคำสั่ง ssl_trusted_certificate ที่จะทำให้ nginx เชื่อ server ของ Let’s Encrypt อีกที (เพราะ nginx ไม่ใช่ browser ครับ มันไม่มีใบรับรองติดมาในตัว)

การสร้างไฟล์ก็ไม่ยากครับ

cat /usr/share/ca-certificates/mozilla/DST_Root_CA_X3.crt /etc/letsencrypt/live/blog.whs.in.th/chain.pem | sudo tee /etc/nginx/letsencrypt_ocsp_chain.pem

(บน Debian)

นั่นคือผมเอา root certificate + Let’s Encrypt Authority X1 แพครวมกันนั่นเองครับ หลังจากนั้น nginx ก็จะสามารถยึนยันตัว server OCSP ได้แล้ว

สรุป

เซตเสร็จแล้วอย่าลืม reload nginx นะครับ หลังจากนั้นเว็บก็พร้อมใช้งาน

ถ้าเซตตามนี้แล้วน่าจะได้ A ที่ SSLLabs.com นะครับ ที่ผมยังงงๆ คือบล็อคผมกับเว็บผมมันใช้ config เดียวกัน แค่อันนึงใช้ SNI อีกอันไม่ใช้ ได้เกรดต่างกันเฉยเลย ก็ยังตามหาอยู่ว่าเกิดอะไร