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

Orix Au Yeung
10 min readMar 6, 2020

--

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嘅方法:

以下係佢提供,放係 /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! 🥰

--

--

Orix Au Yeung

GDG Hong Kong Organizer | Master of Data Science @ UBC | GCP Certified MLE, PCA, PDE, PNE, PCD