nginx+Jupyter: 用reverse proxying 令notebook server 更安全
Hi!有用開Jupyter notebook嘅朋友都應該有遇過一個問題:點樣係localhost以外連接到notebook呢?
我自己冇耐之前都係用Jupyter notebook內置嘅功能去set up個notebook server,但係咁樣係有缺點嘅,譬如:
- 要expose一個port出去dedicate比notebook server。同一個network開多過一個notebook server,就要做mapping/forwarding
- 如果你係用443之外嘅port,打完個網址又要打多個port名。咁做係好煩同容易唔記得
- 安全考慮上,reverse proxy唔直接expose個server IP出去,亦可以當做應用層面嘅firewall
所以今日就介紹下點樣係前面加個nginx reverse proxy更方便同安全地經 Internet access到notebook server。
準備
首先,你要有張SSL cert–有自己domain可以去letsencrypt攞一張,冇嘅自己self-sign一張亦可。(當然冇咁好啦)
之後就當然要裝nginx啦,假設你係用緊Debian-based distro:
sudo apt-get update; sudo apt-get install -y nginx
安裝完之後,可以check下係唔係正常運作緊:
user@your-os:/etc/nginx$ service nginx status
● nginx.service — A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Fri 2020–03–06 18:33:39 HKT; 5h 19min ago
Docs: man:nginx(8)
Process: 27169 ExecStop=/sbin/start-stop-daemon — quiet — stop — retry QUIT/5 — pidfile /run/nginx.pid (code=exited, status=0/SUCCESS)
Process: 27184 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Process: 27170 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Main PID: 27185 (nginx)
Tasks: 7 (limit: 4915)
CGroup: /system.slice/nginx.service
├─27185 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
├─27188 nginx: worker process
├─27190 nginx: worker process
├─27191 nginx: worker process
├─27193 nginx: worker process
├─27194 nginx: worker process
└─27195 nginx: worker process
咁就準備好啦。
Set up Reverse Proxy
JupyterHub本身就有doc提供到set up reverse proxy嘅方法:
以下係佢提供,放係 /etc/nginx/sites-enabled 既config,我地可以攞嚟用(=copy&paste):
# top-level http config for websocket headers
# If Upgrade is defined, Connection = upgrade
# If Upgrade is empty, Connection = close
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# HTTP server to redirect all 80 traffic to SSL/HTTPS
server {
listen 80;
server_name HUB.DOMAIN.TLD;
# Tell all requests to port 80 to be 302 redirected to HTTPS
return 302 https://$host$request_uri;
}
# HTTPS server to handle JupyterHub
server {
listen 443;
ssl on;
server_name HUB.DOMAIN.TLD;
ssl_certificate /etc/letsencrypt/live/HUB.DOMAIN.TLD/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/HUB.DOMAIN.TLD/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/certs/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_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security max-age=15768000;
# Managing literal requests to the JupyterHub front end
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# websocket headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
# Managing requests to verify letsencrypt host
location ~ /.well-known {
allow all;
}
}
未set up ssl_dhparam嘅話,可以行呢個command 用幾分鐘嘅時間generate一個新嘅:
openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
咁當然啦,上邊個snippet凡係有提到HUB.DOMAIN.TLD嘅地方,就要改做你個網址,然後塞埋張SSL cert/chain+private key入去:
server_name HUB.DOMAIN.TLD;
.
.
.
ssl_certificate /etc/letsencrypt/live/HUB.DOMAIN.TLD/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/HUB.DOMAIN.TLD/privkey.pem;
然後proxy_pass入面條URL都要改返notebook server個port(預設係8888):
proxy_pass http://127.0.0.1:8888;
另外,有個tricky位就係Jupyter notebook係需要用HTTP Version 1.1,係/etc/nginx/nginx.conf 要加句特別寫明:
http {
...
proxy_http_version 1.1;
...
}
改好之後,重新啓動nginx比佢load返個config就OK!
sudo /etc/init.d/nginx restart
完成!
辛苦哂,咁就搞掂啦!咁你就用返平時你最鍾意嘅方法去host你個Jupyter notebook server。
對Security有更嚴格要求嘅朋友可以考慮disable埋TLSv1, TLSv1.1同其他比較弱嘅加密法。
去睇下notebook server 嘅SSL Server評分,你可以上 https://www.ssllabs.com/ssltest/ 做個測試🤗