nginx+Jupyter: 用reverse proxying 令notebook server 更安全

Posted on 24 October, 2022

This is originally posted on Medium in 2020. Link to article.

Background from Ben Neale (https://unsplash.com/@ben_neale)


Hi!有用開Jupyter notebook嘅朋友都應該有遇過一個問題:點樣係localhost以外連接到notebook呢?我自己冇耐之前都係用Jupyter notebook內置嘅功能去set up個notebook server,但係咁樣係有缺點嘅,譬如:

  1. 要expose一個port出去dedicate比notebook server。同一個network開多過一個notebook server,就要做mapping/forwarding

  2. 如果你係用443之外嘅port,打完個網址又要打多個port名。咁做係好煩同容易唔記得​

  3. 安全考慮上,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嘅方法:

https://jupyterhub.readthedocs.io/en/stable/reference/config-proxy.html

以下係佢提供,放係 /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/ 做個測試🤗

Secure! 🥰