[譯]通過HTTPS協議執行你的Flask程式

vimiix發表於2019-03-03

我們在開發Flask應用程式時,通常通過執行Flask自帶的Web伺服器來開發測試,這個伺服器提供了基本的但功能完備的WSGI伺服器。但開發結束以後,在應用程式上線到生成環境時,有很多不得不考慮的事情,其中之一是我們是否應該要求客戶端使用加密連線以增加安全性。

人們總是問我這個問題,特別是如何在HTTPS協議上部署Flask伺服器。在本文中,我將介紹幾種為Flask應用程式新增加密的方案,從一個非常簡單的可以在五秒內實現,到一個強大的就像我的網站一樣可以得到一個A +評級解決方案(我的網站的SSL分析資料)。

https://user-gold-cdn.xitu.io/2018/8/27/1657abb15d157b0a?w=629&h=226&f=jpeg&s=51606

HTTPS是如何工作的?

HTTP的加密和安全功能是通過傳輸層安全性(TLS)協議實現的。實際上,TLS定義了一種使任何網路通訊通道安全的標準方法。因為我不是一個安全專家,如果我試著給你一個TLS協議的詳細描述,我認為我無法做到很好,所以我只想告訴你一些我們比較感興趣的內容,要知道我們給Flask伺服器設定安全和加密目的。

一般的思路是,當客戶端與伺服器建立連線並請求加密連線時,伺服器將使用其SSL證照進行響應。證照此時作為伺服器的標識,因為它包括伺服器名和域名資訊。為確保伺服器提供的資訊正確無誤,證照是由證照頒發機構或CA進行加密簽名的。如果客戶端知道並信任CA,則可以確認證照籤名確實來自該實體,並且客戶端可以確定其連線的伺服器是合法的。

客戶端成功驗證證照後,會建立用於與伺服器通訊的加密金鑰。為確保將此金鑰安全地傳送到伺服器,它使用伺服器證照附帶的公鑰對其進行加密。伺服器擁有與證照中的公鑰一起使用的私鑰,因此它是唯一能夠解密包的一方。從伺服器收到加密金鑰時起,所有流量都使用只有客戶端和伺服器知道的金鑰加密。

從上述總結中你可能會猜到,要實現TLS加密,我們需要兩個專案:伺服器證照,包括公鑰並由CA簽名,以及與證照中包含的公鑰一起使用的私鑰。

最簡單的方法

Flask(更具體地說其實是Werkzeug),支援使用即時證照,這對於通過HTTPS快速提供應用程式非常有用,而且不會搞亂系統的證照。你只有需要做的就是將 ssl_context ='adhoc' 新增到程式的 app.run() 呼叫中。遺憾的是,Flask CLI無法使用此選項。舉個例子,下面是官方文件中的“Hello,World” Flask應用程式,並新增了TLS加密:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run(ssl_context='adhoc')
複製程式碼

要在Flask中使用臨時證照,你需要在虛擬環境中安裝一個相關依賴項:

$ pip install pyopenssl
複製程式碼

當你執行指令碼時,就會注意到Flask指示它正在執行 https:// 伺服器:

$ python hello.py
 * Running on https://127.0.0.1:5000/ (Press CTRL+C to quit)
複製程式碼

很簡單,對吧!這樣有一個問題就是瀏覽器不喜歡這種型別的證照,因此瀏覽器會顯示一個大而可怕的警告,你需要在訪問應用程式之前先手動允許才可以訪問。一旦允許瀏覽器連線,你就擁有了一個加密連線,就像從具有有效證照的伺服器獲得的連線一樣,這使得這些臨時證照便於開發中進行快速和驗證測試,但不能用於任何實際生產環境中。

自簽名證照

所謂的自簽名證照是使用與同一證照關聯的私鑰生成簽名的證照。我在上面提到客戶端需要“知道並信任”簽署證照的CA,因為這個信任關係允許客戶端驗證伺服器證照。Web瀏覽器和其他HTTP客戶端一般都預先配置了一個已知和可信CA的列表,但顯然如果你使用自簽名證照,CA將不會被知道,驗證也必將失敗。這正是我們在上一節中使用的臨時證照所發生的情況。如果Web瀏覽器無法驗證伺服器證照,它將允許你手動來繼續並訪問相關網站,但這將確保你自己必須瞭解你所承擔的風險。

[譯]通過HTTPS協議執行你的Flask程式

但是,到底風險是什麼呢?使用上一節中的Flask伺服器來說,顯然我們是相信自己的,所以對我們沒有任何風險。主要問題是當使用者在連線到他們不瞭解或不是他們控制的站點時出現此警告。在這些情況下,使用者無法知道伺服器是否可信,因為任何人都可以為任何域生成證照,正如接下來你要看到的。

雖然自簽名證照有時很有用,但Flask的臨時證照並不是那麼好,因為每次伺服器執行時,都會通過pyOpenSSL動態生成不同的證照。使用自簽名證照時,最好每次啟動伺服器時都使用相同的證照,因為這樣可以將瀏覽器配置為信任它,從而消除了安全警告。

我們可以從命令列很輕鬆生成自簽名證照。只需要安裝了openssl

openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365
複製程式碼

這個命令在cert.pem中寫入新證照,其中相應的私鑰位於key.pem中,有效期為365天。執行此命令時,系統會詢問你幾個問題(譯者注:用於生成證照的所有者資訊)。下面你可以看到我如何回答它們為我自己的私有伺服器 生成證照:

Generating a 4096 bit RSA private key
......................++
.............++
writing new private key to 'key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Beijing
Locality Name (eg, city) []:Beijing
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Vimiix
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:vimiix
Email Address []:contact@vimiix.com
複製程式碼

我們現在可以在Flask應用程式中使用這個新的自簽名證照,方法是將 app.run() 中的 ssl_context 引數設定為具有證照和私鑰檔案的檔名的元組:

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run(ssl_context=('cert.pem', 'key.pem'))
複製程式碼

瀏覽器將繼續給出警告這個證照,但如果你檢查它,你就將看到你在建立時輸入的資訊:

[譯]通過HTTPS協議執行你的Flask程式

用於生產的Web伺服器

當然我們都知道Flask開發伺服器只適合開發和測試。那麼我們如何在生產伺服器上安裝SSL證照呢?

如果您使用的是gunicorn,可以使用命令列引數執行此操作:

$ gunicorn --certfile cert.pem --keyfile key.pem -b 0.0.0.0:8000 hello:app
複製程式碼

如果你使用 nginx 作為反向代理,那麼你可以使用 nginx 配置證照,然後 nginx 可以 “全權代理” 加密連線,這意味著它將接受來自外部的加密連線,然後使用常規的未加密連線與我們的後端服務互動。nginx的配置項如下:

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    # ...
}
複製程式碼

還需要考慮的另一個重要問題是如何讓 nginx 處理通過普通的 HTTP連線的客戶端。在我看來,最好的解決方案是通過重定向到相同的URL,使用HTTPS來響應未加密的請求。對於Flask應用程式,您可以使用Flask-SSLify擴充套件來實現。對於使用nginx,我們可以在配置中包含另一個伺服器模組:

server {
    listen 80;
    server_name example.com;
    location / { # 全部轉發到https的相同url下
        return 301 https://$host$request_uri;
    }
}
複製程式碼

如果你使用的是其他Web伺服器,請檢視其文件,應該會找到類似的方法來建立上面顯示的配置。

使用“真實”證照

我們現在已經瞭解了自簽名證照的所有可選方案,但在所有這些情況下,有共同的限制就是Web瀏覽器仍然不會信任這些證照,除非你手動接受他們,因此生產伺服器上的證照的最佳選擇站點應該是從一個眾所周知並且由所有Web瀏覽器自動信任的CA中獲取一個證照。當你從某個CA請求證照時,這個機構將會驗證你是否可以控制你的伺服器和域,但驗證的執行方式取決於CA。如果你的伺服器通過驗證,則CA將為你的伺服器頒發具有該CA簽名的證照,並將其提供給你進行安裝。證照一般在一段時間內有效,通常不會超過一年。大多數CA都會為這些證照收取費用,但有一些CA會免費提供這些證照。最受歡迎的免費CA就是 :Let's Encrypt

從 Let's Encrypt 獲取證照相當容易,因為整個過程是自動化的。假設你使用的是基於Ubuntu的伺服器,則必須首先在伺服器上安裝其開源 certbot 工具:

$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install certbot
複製程式碼

現在你已準備好請求證照的前置工作了。certbot 有幾種方法可用於驗證你的網站。通常,“webroot” 方法最容易實現。使用此方法,certbot 會在Web伺服器公開的目錄中新增一些檔案作為靜態檔案,然後嘗試使用你希望為其生成證照的域名,通過HTTP訪問這些檔案。如果此測試成功,則 certbot 知道執行它的伺服器與正確的域名相關聯,並且滿足併發布證照。使用此方法請求證照的命令如下

$ sudo certbot certonly --webroot -w /var/www/example -d example.com
複製程式碼

在這個例子中,我們正在嘗試為 example.com 域名生成證照,該域名使用 /var/www/example 中的目錄作為靜態檔案根目錄。如果 certbot 能夠驗證域名,它將把證照檔案寫為 /etc/letsencrypt/live/example.com/fullchain.pem,將私鑰寫為 /etc/letsencrypt/live/example.com/privkey.pem,這些證照在90天內有效。

要使用這個新獲取的證照,你可以輸入上面提到的兩個檔名來代替我們之前使用的自簽名檔案,這應該適用於上述任何配置。當然,你還需要保證驗證的域名能正常訪問到你的應用程式,因為這是瀏覽器接受證照有效的唯一方式。

如果你使用 nginx 作為反向代理,則可以在配置中建立對映,為certbot提供一個私有目錄,以便在其中編寫其驗證檔案。在下面的示例中,我擴充套件了上一節中顯示的HTTP伺服器塊,以將所有 Let's Encrypt 的相關請求傳送到你選擇的特定目錄:

server {
    listen 80;
    server_name example.com;
    location ~ /.well-known {
        root /path/to/letsencrypt/verification/directory;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}
複製程式碼

當需要續訂證照時,也會使用Certbot。為此,你只需執行以下命令:

$ sudo certbot renew
複製程式碼

如果系統中有任何證照即將過期,則執行上述命令會更新它們,並確保新證照保留在相同位置。如果希望獲取續訂的證照,則可能需要重新啟動Web伺服器。

獲得SSL A+認證

如果你使用 Let's Encrypt 的證照或其他已知CA的證照,並且你在此伺服器上執行最近維護的作業系統,那麼就SSL安全性而言,你可能已經擁有了很接近高評分的伺服器。您可以前往Qualys SSL Labs網站並獲取報告,瞭解你伺服器的排名。

接下來還有一點小事需要做,該報告將指出你需要改進的地方,但總的來說,我希望你會被告知伺服器為加密通訊公開的選項太寬泛或太弱,讓你對已知漏洞持開放態度。

相對容易進行改進的一個地方是如何生成在加密金鑰交換期間使用的係數,其通常具有相當弱的預設值。特別是,Diffie-Hellman係數需要相當長的時間才能生成,因此預設情況下伺服器使用較小的數字來節省時間。但是我們可以預先生成強係數並將它們儲存在一個檔案中,然後 nginx 就可以使用它。使用openssl工具,你可以執行以下命令:

openssl dhparam -out /path/to/dhparam.pem 2048
複製程式碼

如果你想要更強的係數,你可以改變2048或更大,如4096。此命令需要一段時間才能執行,特別是如果你的伺服器CPU數量不多的話,但是當它完成後,你就會擁有一個具有強係數的 dhparam.pem檔案,你可以將其插入到 nginx 的 ssl伺服器模組中:

	ssl_dhparam /path/to/dhparam.pem;
複製程式碼

接下來,你可能需要配置伺服器允許加密通訊的加密方式。這是我伺服器上的加密列表:

	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:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:!DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
複製程式碼

在這個列表中,禁用的密碼以 ! 為字首。SSL報告將告訴你是否存在不推薦的密碼。你必須不時地關注是否發現了需要修改這個加密列表的新漏洞。

你可以在下面找到我當前的 nginx SSL 配置,其中包括上述設定,以及我為解決SSL報告中的警告而新增的一些設定:

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    ssl_dhparam /path/to/dhparam.pem;
    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:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:!DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_stapling on;
    ssl_stapling_verify on;
    add_header Strict-Transport-Security max-age=15768000;
    # ...
}
複製程式碼

在文章開頭,你可以看到我為我的網站獲得的評分結果。如果你在所有類別中都達到100%標記,則必須為配置新增更多的限制,但這也將限制可以連線到你的站點的客戶端數量。通常,較舊的瀏覽器和HTTP客戶端使用的是不是很強的加密方式,但如果禁用這些加密方式,則這些客戶端將無法連線。(譯者注:安全性與相容性的選擇,作為服務提供方應該考慮這一點,適當的降低服務的限制,來相容較舊的請求方式,也是一種妥協。)因此,你基本上需要妥協,並且還需要定期檢視安全報告,並隨著時間的推移進行更新修正。

遺憾的是,對於這些最後的SSL改進的複雜程度,你必須使用專業級Web伺服器,那麼如果你不想使用nginx,要找到一個支援這些設定的web服務,可選擇的數量目前是很少的。我知道 Apache 支援,但除此之外,我不知道誰家還支援。

原文:Running Your Flask Application Over HTTPS --- miguelgrinberg

--- EOF ---

相關文章