1 頁 (共 1 頁)

用 nginx 建置一個 A+ 等級的 https 網頁伺服器

發表於 : 2015-12-10 23:45:01
yehlu
https://www.peterdavehello.org/2015/10/ ... e-version/

隨著資安意識提升、 Google 把網站的 https 列為搜尋引擎的排行指標,越來越多的網站開始導入 https 以確保伺服器以及使用者端兩個端點之間的安全溝通,先前在 10 web server online https/ssl testing services 有列出了一些可以協助網管人員測試網頁伺服器安全性強度的服務(注意是”網頁伺服器”而不是”網頁應用程式”),讓大家可以參考看看,其中 Qualys SSL Labs 的 SSL Server Test 算是近期非常熱門的一個測試跟服務,其測試報告以及評分標準算是非常簡單易懂,以截至目前為止(2015年10月25日)的最新版本”2009j (20 May 2015)“為例,給分主要從 A ~ F,Protocol support、Key exchange 及 Cipher strength 分別占總分的30%、30%及40%,相關的細節都可以在SSL Server Rating Guide (PDF) – Qualys SSL Labs 裡面找到,Qualys SSL Labs也提供了一份 SSL/TLS Deployment Best Practices Guide,但看起來近期沒更新就是了,停留在Version 1.4 / 8 December 2014。

對於一些非網管或是相關背景的網站管理員來說,該如何有效的提升自己架設的伺服器安全性強度? 又如何改善各安全測試出來的分數? 由於最近有些人在問相關的問題,我找了一下發現好像沒有中文的資源在提供這方面的指南,所以決定野人獻曝一下稍微分享我知道的做法。另外必須說明的是,安全性跟方便性從以前到現在就是兩難,例如夠安全的密碼基本上都是由不同的元素以及夠長的長度組成,相對來講就會不好記,在伺服器安全性上的問題亦然,較好的安全性會使得Windows XP,Java 6的使用者受到影響,如果還有遇到這使用如此老舊軟體的使用者,還是勸他趕緊換個平台吧 …

開頭先說一個比較不影響這次評分(https)但讀者可能也會想要處理的一塊,關於 Web server 的 response header 處理的部分,可以參考先前寫的”用Apache/nginx&PHP架網站要注意的安全事項“,將不必要的Server資訊隱藏起來,那接下來就會講這次的重點,關於伺服器的安全性設定以及該如何”拉分”!


注意:我不是這方面的專家,內容無法保證正確而且很可能有部份內容無法使用適當的方法或是專業用語描述,若有任何可以改進的地方都歡迎大家指正、討論,而這次介紹的重點也不在這些通訊協定、加密演算法後面的原理或是理論,而是在分享該如何在不考慮、或是沒有專業背景的情況下提升伺服器的安全強度,而我也相信不是所有的網管人員都有非常”完整”的相關背景並且”持續”在更新相關的知識、技能,這也是為什麼我們需要透過測試服務來告訴我們有哪些已知的問題需要改善的原因。



本篇示範的環境:

作業系統:Ubuntu Server 14.04.3 LTS 64bit
nginx:1.4.6-1ubuntu3.3 (從Ubuntu Trusty的套件庫透過apt-get/aptitude安裝)
備註:
http/https使用的是預設的80/443連接埠
我是使用 Virtualbox 4.3.32這個虛擬機環境作來建立這次的示範主機,至於為什麼不使用 v5.x? 因為我的 Virtualbox 跑在 FreeBSD,而 FreeBSD 的 Virtualbox v5.x還沒準備好 …
該怎麼設定https的相關教學,網路上已經有相當多了,一些常見的簽證服務也都有提供教學,這邊只大略列幾個重點:

使用 OpenSSL 產生 Private Key 及 CSR (Certificate Signing Request,證書的簽名請求)
請注意
RSA的key請直接使用2048或4096,不要使用1024bits
簽名演算法請直接使用sha256或以上,不要再使用SHA-1
目前不會被扣分,應該很快就會了 …
openssl req -out domainA.csr -new -newkey rsa:2048 -sha256 -nodes -keyout domainA.key
把 CSR 送去申請憑證回來
請注意,要回來的憑證也有分為sha1、sha256,請使用sha256
有些廠商可能沒得選,那就沒轍了,但新的應該都是sha256了
把憑證跟私鑰設進Web server
以nginx為例會是長這樣 (以本次環境為例預設在/etc/nginx/sites-enabled/default,看你有沒有拆分多個virtual host)
ssl_certificate /etc/nginx/ssl/domainA.crt;
ssl_certificate_key /etc/nginx/ssl/domainA.key;
接下來才是本篇的重點,這邊的環境以Ubuntu Trusty(14.04.x)套件庫中的nginx安裝好的預設環境為基礎進行,而SSL Certificate設定好後我將進行第一次的測試。

第一次測試結果如下,rank C:

ssl_demo_test_1

接下來分成兩個部分來講,一個是Certificate(Authentication)的部分,一個是Configuration的部分,Certificate的部分會晚點講,因為這個不是設定問題,假使幫忙簽證書的廠商有幫忙打包好上面的憑證則這部分就不會有問題。



Configuration的部分

測試結果面上方的Summary框框是重點,下方有幾個淺色的橘、紅色框框,分別列出幾個被扣分的重點:

This server supports weak Diffie-Hellman (DH) key exchange parameters. Grade capped to B.
This server is vulnerable to the POODLE attack. If possible, disable SSL 3 to mitigate. Grade capped to C.
This server’s certificate chain is incomplete. Grade capped to B. (Certificate的部分,晚點說)
Okay,只有兩個問題,第一個 Weak Diffie-Hellman 好解決,就是 Diffie-Hellman 預設質數長度不夠的問題,做法是使用下面的 openssl 命令產升 2048-bit “Strong” Diffie Hellman Group (Intel Core i5-4570實測約需一分鐘半):


$ openssl dhparam -out dhparams.pem 2048
1
$ openssl dhparam -out dhparams.pem 2048
CPU單執行緒速度夠快或是不趕時間也可以使用 4096 bit 的質數,理論上更為安全 (Intel Core i5-4570實測約需五分鐘)


$ openssl dhparam -out dhparams.pem 4096
1
$ openssl dhparam -out dhparams.pem 4096
註:這邊會花比較多時間,按照主機效能不同,大約需要數十秒()~數分鐘不等,如果是租用低階的VPS虛擬主機可能會花費更多的時間!請耐心等待!

產生出來的 dhparams.pem 請把它當作你其他的 private key 一樣好好保管,安全起見請調整檔案的owner, group以及權限,例如:


$ sudo chown www-data:www-data dhparams.pem
$ sudo chmod 400 dhparams.pem
1
2
$ sudo chown www-data:www-data dhparams.pem
$ sudo chmod 400 dhparams.pem
接下來設定 nginx,找到 nginx 設定網站的地方 (預設在/etc/nginx/sites-enabled/default),在server區段的部份,也就是server { … } 這裡面,放入底下這一行:

ssl_dhparam /etc/nginx/ssl/dhparams.pem;

註:我習慣上擺在ssl_certificate、ssl_certificate_key下一行,請注意把斜線部分改為你對應的檔案路徑。

接下來讓 nginx 重新載入設定,這部分就算完成了!


$ sudo service nginx reload
1
$ sudo service nginx reload
由於 Qualys SSL Labs 的 SSL Server Test 測試項目眾多、跑起來比較花時間(大約兩分鐘),這個項目調整完後我們可以到令一個網站進行單一項目的檢測:

https://weakdh.org/sysadmin.html (Server Test – Guide to Deploying Diffie-Hellman for TLS)
測試結果長這樣,截圖供大家參考:

Weak Diffie-Hellman:ssl_demo_weakdh_test_failed
Strong Diffie-Hellmanssl_demo_weakdh_test_pass
Weak Diffie-Hellman 處理完後換 POODLE(CVE-2014-3566),這是一個在去年底(2014年10月左右)被發現的一種中間人攻擊漏洞,參考 Security Labs 的社群部落格內容 – SSL 3 is dead, killed by the POODLE attack ,基本上這是一個 protocol level 的問題,解法是棄用 SSL 3 或是關掉 CBC-mode cipher,但只關掉CBC還要用的SSL3的話只剩下另一個以知也很多洞的RC4 … 所以最終解法就是把SSLv3丟掉。

nginx預設支援的protocols設定長這樣(一樣在設定檔的server區塊):

ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;

拿掉後SSLv3的就 … 沒什麼好說就是這樣XD:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

因為 Qualys SSL Labs 的 SSL Server Test 跑起來真的很久 … 這邊可以透過curl或是搬另外一套服務來測POOGLE:

curl 命令(如果不是在同一台機器上請自行把IP換掉):


$ curl -kI <strong>-v3</strong> https://127.0.0.1
1
$ curl -kI <strong>-v3</strong> https://127.0.0.1
SSLv3 被拿掉的結果:
* Rebuilt URL to: https://127.0.0.1/
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1…
* Connected to 127.0.0.1 (127.0.0.1) port 443 (#0)
* successfully set certificate verify locations:
* CAfile: none
CApath: /etc/ssl/certs
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS alert, Server hello (2):
* error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure
* Closing connection 0
curl: (35) error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure



SSLv3還在的話結果會類似這樣:
* successfully set certificate verify locations:
* CAfile: none
CApath: /etc/ssl/certs
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Server key exchange (12):
* SSLv3, TLS handshake, Server finished (14):
* SSLv3, TLS handshake, Client key exchange (16):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSL connection using ECDHE-RSA-AES256-SHA
* Server certificate:
* subject: CN=demo.peterdavehello.org

後面還有一長串 … 略 …

不想用命令測的話可以來這邊:

Test for POODLE vulnerability · SSL-Tools – https://ssl-tools.net/poodle-test

測試結果:

SSLv3沒關ssl_demo_poodle_test_failed.png
SSLv3關掉:ssl_demo_poodle_test_pass.png
到這邊 … 基本的設定就算完成了,沒意外的話應該可以拿一個Rank B:

ssl_demo_test_2_rank_B

如果有人想知道怎麼用 openssl 來測的話 … 命令長這樣,細節就不講了,有興趣的人自己研究吧 :D


$ openssl s_client -connect 127.0.0.1:443 -ssl3
1
$ openssl s_client -connect 127.0.0.1:443 -ssl3


Certificate(Authentication)的部分

接下來講Certificate的部分,也就是測試結果Summary上的這一點:

This server’s certificate chain is incomplete. Grade capped to B.
將網頁稍微往下拉看一下:
ssl_demo_test_cert_chain_issue

簡單來說,除了根憑證(root)可以是自己簽(核發單位自己簽,不是我們自己簽)以外,其他憑證都是要靠相關單位層層簽署,而我們最好要把中間簽署者的憑證也一並提供出來,出現這樣的警告通常是由於憑證提供廠商預設沒有把”根憑證”與”我們的憑證”之間的憑證給包進憑證中,而我們又沒有手動把這些憑證放進去,導致檢查的過程中,client端需要自行下載(Extra download)中間的憑證,如果在封閉網路或是一些較舊的手持式裝置遇到這樣的情況,則憑證很有可能被視為無法信任的憑證,處理方式就是自己打包出包含中介憑證的憑證 … 有點饒舌(應該是有什麼專業的用語但我不知道)。

這部分中間需要的憑證需要到提供憑證的廠商網站下載,通常在申請時會有相關的提示,拿到憑證後做法大概長這樣:


cat 3_demo.peterdavehello.org.crt 2_issuer_Intermediate.crt 1_cross_Intermediate.crt &gt; demo.peterdavehello.org.bundle.crt
1
cat 3_demo.peterdavehello.org.crt 2_issuer_Intermediate.crt 1_cross_Intermediate.crt &gt; demo.peterdavehello.org.bundle.crt
這邊的例子來說,我有三張憑證需要拿出來:

簽給我的憑證
3_demo.peterdavehello.org.crt
幫我簽憑證的廠商的憑證
2_issuer_Intermediate.crt
幫”幫我簽憑證的廠商“交互簽證的廠商的憑證
1_cross_Intermediate.crt
自己的cert放最前面,放錯了server可能會不讓你開,中間順序就看是誰幫誰簽依序往下,用shell redirect的方式組成一個新的檔案,開記事本複製貼上也可以啦 … 習慣上這種綁了好幾個憑證的檔案會用bundle來命名,接著讓nginx去吃新的檔案:

舊設定:ssl_certificate /etc/nginx/ssl/domainA.crt;
新設定:ssl_certificate /etc/nginx/ssl/domainA_bundle.crt;
一樣重新載入nginx設定後跑測試,跟我一樣嫌 Qualys SSL Labs 的 SSL Server Test慢的人可以先到這邊測:https://ssl-tools.net/webservers/,其他常見測試一樣都有,但沒有Ranking就是。

測試結果:

調整前:
ssl_demo_test_cert_chain_issue_ssltools
調整後:
ssl_demo_test_cert_chain_issue_fixed_ssltools
再回 Qualys SSL Labs SSL Server Test 跑一次就可以拿Rank A了:
ssl_demo_test_cert_chain_issue_fixed

講到最後 … 要怎麼拿 A+ 呢?

根據這張圖 …
qualys_ssl_labs_2

我們應該要在 Response Header 裡面設定一個叫做 Strict-Transport-Security (HSTS) 的玩意兒,來告訴 client 說我們接下來的溝通都使用 https 吧,透過 HSTS header,接下來的特定時間長度內(max-age)的連線將會自動採用 https,如以一來可以:

避免中間人攻擊
使用者通常不會自行指定使用https,而是透過http再redirect,此階段是不安全的
中間人攻擊雖然很有可能造成憑證無效,但通常使用者會自行忽略警告 … 夠過HSTS設定,使用者將不再被允許忽略警告
省去redirect成本
在nginx裡面的設定方法為在設定檔的server區段插入下列設定(max-age為設定有效的秒數,建議至少一年以上):

add_header Strict-Transport-Security "max-age=63072000;";

如果確認所有子網域也已經啟用https,可以改使用:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";

如此一來,子網域的安全性將受到更安全的保障,避免第一次的http -> https redirect時的中間人攻擊。

一樣的 … 改完設定後讓nginx重新載入設定,然後,來跑測試吧!

這邊有一個更簡單的測法!


$ curl -kI https://127.0.0.1
1
$ curl -kI https://127.0.0.1
如果得到的結果裡面有看到 Strict-Transport-Security … 就表示成功了!

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 25 Oct 2015 16:36:18 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 04 Mar 2014 11:46:45 GMT
Connection: keep-alive
ETag: “5315bd25-264”
Strict-Transport-Security: max-age=63072000;
Accept-Ranges: bytes

沒問題就來跑 Qualys SSL Labs SSL Server Test 了:
https://www.ssllabs.com/ssltest/

最後這邊附上兩個網站:

Cipherli.st – Strong Ciphers for Apache, nginx and Lighttpd
https://cipherli.st/
Source code (GitHub)
https://github.com/RaymiiOrg/cipherli.st
Mozilla SSL Configuration Generator
https://mozilla.github.io/server-side-t ... generator/
Source code (GitHub)
https://github.com/mozilla/server-side-tls
裡面整理了常見的 server 端 SSL config,程式碼都丟在GitHub 上面並且有持續在更新,除了上述所說的設定以外,更包含了Cipher的設定(加密要使用哪些演算法),正確的使用可以有效提升網站的安全性。本次設定沒有動到Cipher的原因在於使用的作業系統還不算太過時,使用的nginx來自於作業系統上游持續在維護、更新的套件庫,所以預設的cipher(ssl_ciphers “HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES”;)強度是沒有問題的,但現在沒問題並不保證以後沒問題,還是要定期測試、檢視網站的設定才行!另外這兩個網站裡面還有其他有助於提升安全性的設定,因為不影響這次測試分數所以沒有特別提到,但建議大家還是可以看看並加入自己的設定檔中。

附上一張從預設值的 Rank C 到調整後 Rank A+ 的測試結果吧!
(有興趣也可以看裏面一些細節,例如Client相容性等)
ssl_demo_test_a_plus

同場加映我目前預設使用的設定(for nginx v1.4.6),有些東西是前面沒提到的,就給大家參考看看囉:


server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
server_name demo.peterdavehello.org;
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server ipv6only=on;
ssl_certificate /etc/nginx/ssl/demo/demo.peterdavehello.org.bundle.crt;
ssl_certificate_key /etc/nginx/ssl/demo/demo.peterdavehello.org.key;
ssl_dhparam /etc/nginx/ssl/demo/dhparam.pem;
ssl_session_cache shared:SSL:9m;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
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_stapling on;
ssl_stapling_verify on;
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
location ~ /\.ht {
deny all;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
server_name demo.peterdavehello.org;
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server ipv6only=on;
ssl_certificate /etc/nginx/ssl/demo/demo.peterdavehello.org.bundle.crt;
ssl_certificate_key /etc/nginx/ssl/demo/demo.peterdavehello.org.key;
ssl_dhparam /etc/nginx/ssl/demo/dhparam.pem;
ssl_session_cache shared:SSL:9m;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
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_stapling on;
ssl_stapling_verify on;
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
location ~ /\.ht {
deny all;
}
}