CUBE SUGAR CONTAINER

技術系のこと書きます。

リモートサーバの Jupyter Notebook を SSH Port Forwarding 経由で使う

一般的に Jupyter Notebook はローカルの環境にインストールして使うことが多い。 ただ、ローカルの環境は計算資源が乏しい場合もある。 そんなときは IaaS などリモートにあるサーバで Jupyter Notebook を使いたい場面が存在する。 ただ、セキュリティのことを考えると Jupyter Notebook の Web UI をインターネットに晒したくはない。

そこで、今回は SSH Port Forwarding を使って Web UI をインターネットに晒すことなく使う方法について書く。 このやり方ならリモートサーバに SSH でログインしたユーザだけが Jupyter Notebook を使えるようになる。 また、Web UI との通信も SSH 経由になるので HTTP over SSL/TLS (HTTPS) を使わなくても盗聴のリスクを下げられる。

リモートサーバを想定した環境は次の通り。 話を単純にするために環境は Vagrant で作ってある。

vagrant $ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS"
vagrant $ uname -r
4.15.0-29-generic

そこに接続するクライアントの環境は次の通り。

client $ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.13.6
BuildVersion:   17G65
client $ ssh -V
OpenSSH_7.6p1, LibreSSL 2.6.2

必要なパッケージをインストールする

ここからは、すでにリモートの Ubuntu マシンに SSH でログインしている前提で話を進める。

まずは必要なパッケージをインストールする。 ログインするたびに Jupyter Notebook を起動するコマンドを入力するのも面倒なので、最終的に Supervisord でデーモン化することにした。

vagrant $ sudo apt-get update
vagrant $ sudo apt-get -y install jupyter-notebook supervisor

今回は OS のパッケージ管理システム経由でインストールしてるけど pip を使うとかはお好みで。

まずはサクッと試す

ひとまず手っ取り早く今回やることの本質を示す。

最初にリモートサーバ上で Jupyter Notebook を起動する。 これで TCP/8888 で Jupyter Notebook の Web UI が動く。

vagrant $ jupyter notebook

ターミナルに Web UI のアクセストークンが表示されるのでメモしておこう。

続いて、クライアントの別のターミナルを開いて、改めてリモートサーバに SSH でログインする。 このとき SSH Port Forwarding を使って、リモートサーバの TCP/8888 をローカルホストのポートにマッピングする。

client $ ssh -L 8888:localhost:8888 <username>@<remotehost>

今回は Vagrant の環境を使っているのでこんな感じ。 恒久的に設定を入れたいなら Vagrantfile を編集する。

client $ vagrant ssh-config > ssh.config
client $ ssh -L 8888:localhost:8888 -F ssh.config default

あとは、クライアントのブラウザでローカルホストにマッピングしたポート番号を開くだけ。

client $ open http://localhost:8888

すると、Jupyter Notebook の Web UI でアクセストークンを入力する画面が表示される。 先ほど Jupyter Notebook を起動するときにターミナルに表示されたトークンを入力しよう。

f:id:momijiame:20181015080902p:plain

これで、いつもの見慣れた Web UI が表示されるはず。 あとは使うだけ。

f:id:momijiame:20181014022447p:plain

以上で、今回やることの本質は示せた。

ただ、上記の操作は毎回やるには結構めんどくさいしセキュリティをあまり考慮していない。 そこで、ここからは運用をできるだけ楽に、そしてセキュアな環境を手に入れるべく手順を記載していく。

以降の手順を試すときは、一旦先ほど起動した Jupyter Notebook は停止しておこう。

アクセス制御をかける

リモートサーバを想定しているので、念のため必要なポート以外はファイアウォールを使って閉じておく。

SSH に使うポートだけを残して、それ以外は全て閉じる。 SSH に使うポート番号を 22 以外にしているときは、適宜読み替える感じで。

vagrant $ sudo ufw allow 22
vagrant $ sudo ufw default DENY
vagrant $ yes | sudo ufw enable
vagrant $ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22                         ALLOW       Anywhere                  
22 (v6)                    ALLOW       Anywhere (v6)             

ファイアウォールの設定を変更するときはリモートサーバから追い出されないように注意しよう。

Jupyter Notebook を起動するユーザを追加する

若干好みの問題にも近いけど、念のため Jupyter Notebook を起動する専用のユーザを追加しておく。

vagrant $ sudo useradd -m -s $SHELL jupyter

Jupyter Notebook を設定する

ここからは Jupyter Notebook を設定していく。

まずは先ほど作ったユーザにログインする。

vagrant $ sudo su - jupyter

続いて、設定ファイルを生成する。

jupyter $ jupyter notebook --generate-config
Writing default config to: /home/jupyter/.jupyter/jupyter_notebook_config.py

Jupyter Notebook の作業ディレクトリを用意する。

jupyter $ mkdir -p /home/$(whoami)/jupyter-working

設定ファイルを編集する。

jupyter $ sed -i.back \
  -e "s:^#c.NotebookApp.token = .*$:c.NotebookApp.token = u'':" \
  -e "s:^#c.NotebookApp.ip = .*$:c.NotebookApp.ip = 'localhost':" \
  -e "s:^#c.NotebookApp.open_browser = .*$:c.NotebookApp.open_browser = False:" \
  -e "s:^#c.NotebookApp.notebook_dir = .*$:c.NotebookApp.notebook_dir = '/home/$(whoami)/jupyter-working':" \
  /home/$(whoami)/.jupyter/jupyter_notebook_config.py
jupyter $ cat ~/.jupyter/jupyter_notebook_config.py | sed -e "/^#/d" -e "/^$/d"
c.NotebookApp.ip = 'localhost'
c.NotebookApp.notebook_dir = '/home/jupyter/jupyter-working'
c.NotebookApp.open_browser = False
c.NotebookApp.token = u''

それぞれの設定の内容や意図としては以下のような感じ。

  • c.NotebookApp.ip = 'localhost'
    • Jupyter Notebook が Listen するアドレスをループバックアドレスにする
    • もしファイアウォールがなくてもインターネットからは Jupyter Notebook の WebUI に疎通がなくなる
  • c.NotebookApp.notebook_dir = '/home/jupyter/jupyter-working'
    • Jupyter Notebook の作業ディレクトリを専用ユーザのディレクトリにする
    • 仮に Web UI が不正アクセスを受けたときにも影響範囲を小さくとどめる (気休め程度)
  • c.NotebookApp.open_browser = False
    • 起動時にブラウザを開く動作を抑制する
    • ローカル環境ではないので起動するときにブラウザを起動する必要はない
  • c.NotebookApp.token = u''
    • Jupyter Notebook の Web UI にビルトインで備わっている認証を使わない
    • 認証は SSH によるログインで担保する場合の設定 (心配なときは後述する共通パスワードなどを設定する)

(オプション) Jupyter Notebook の Web UI に共通パスワードをかける

SSH のログイン以外にも認証をかけたいときは、例えばシンプルなものだと共通パスワードが設定できる。

Jupyter Notebook の Web UI に共通パスワードをかけるには jupyter notebook password コマンドを実行する。

jupyter $ jupyter notebook password
Enter password: 
Verify password: 
[NotebookPasswordApp] Wrote hashed password to /home/jupyter/.jupyter/jupyter_notebook_config.json

すると、ソルト付きの暗号化されたパスワードが設定ファイルとしてできる。

jupyter $ cat ~/.jupyter/jupyter_notebook_config.json 
{
  "NotebookApp": {
    "password": "sha1:217911554b0b:f2fa9cd9f336951c335bdaa06a6c16eb6286c192"
  }
}

上記のやり方だとハッシュのアルゴリズムが SHA1 固定っぽい。 もし、より頑丈なものが使いたいときは次のように Python のインタプリタ経由で生成する。

jupyter $ python3
Python 3.6.6 (default, Sep 12 2018, 18:26:19) 
[GCC 8.0.1 20180414 (experimental) [trunk revision 259383]] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from notebook.auth import passwd
>>> passwd('jupyter-server-password', algorithm='sha512')
'sha512:d197670d2987:19bb2eedfc6fde56f1a9fc04d403999c3f03a99af368e528f45ee9a68f01a7c5f07e375bd34ec176d1c66a0f2e8ef7615ebcf9e524a23ace5ab6dd5a930398d4'

生成した暗号化済みパスワードを、次のような形で Jupyter Notebook の設定ファイルに入力すれば良い。

c.NotebookApp.password = u'sha512:d197670d2987:19bb2eedfc6fde56f1a9fc04d403999c3f03a99af368e528f45ee9a68f01a7c5f07e375bd34ec176d1c66a0f2e8ef7615ebcf9e524a23ace5ab6dd5a930398d4'

上記の共通パスワード方式を含む Jupyter Notebook の認証周りについては以下の公式ドキュメントを参照のこと。

Running a notebook server — Jupyter Notebook 5.7.0 documentation

Jupyter Notebook を Supervisord 経由で起動する

続いては Jupyter Notebook をデーモン化する設定に入る。

一旦、元の管理者権限をもったユーザに戻る。

jupyter $ exit
logout

Supervisord の設定ファイルを用意する。

vagrant $ cat << 'EOF' | sudo tee /etc/supervisor/conf.d/jupyter.conf > /dev/null
[program:jupyter]
command=jupyter notebook
user=jupyter
stdout_logfile=/var/log/supervisor/jupyter.log
redirect_stderr=true
autostart=true
autorestart=true
EOF

Supervisord を起動する。

vagrant $ sudo systemctl enable supervisor
vagrant $ sudo systemctl reload supervisor

ちゃんと Jupyter Notebook が起動しているかを確認する。

vagrant $ ps auxww | grep [j]upyter
jupyter   4689 27.0  5.4 183560 55088 ?        S    16:31   0:01 /usr/bin/python3 /usr/bin/jupyter-notebook
vagrant $ ss -tlnp | grep :8888
LISTEN   0         128               127.0.0.1:8888             0.0.0.0:*       
LISTEN   0         128                   [::1]:8888                [::]:*       

もし、上手く立ち上がっていないときはログから原因を調べよう。

vagrant $ sudo tail /var/log/supervisor/supervisord.log 
vagrant $ sudo tail /var/log/supervisor/jupyter.log 

(オプション) ログインシェルを無効化する

もし Jupyter Notebook 専用に作ったユーザをシェル経由で操作するつもりがなければ、ログインシェルを無効化しておく。

vagrant $ sudo usermod -s /usr/sbin/nologin jupyter

こうするとシェル経由でユーザにログインできなくなる。

vagrant $ grep jupyter /etc/passwd
jupyter:x:1001:1001::/home/jupyter:/usr/sbin/nologin
vagrant $ sudo su - jupyter
This account is currently not available.

デーモンプログラムを起動するユーザは、不正アクセスを受けた場合の影響を小さくする意図でこうすることが多い。

SSH Port Forwarding 経由で Jupyter Notebook の Web UI にアクセスする

ここまでで、リモートサーバ上の Jupyter Notebook の設定は終わった。

一旦リモートサーバから SSH でログアウトする。

vagrant $ exit

改めて SSH Port Forwarding を有効にしてリモートサーバにログインする。 このときリモートサーバの TCP/8888 ポートを、ローカルホストのポートにマッピングする。 ユーザ名やホスト名は適宜読み替える。

client $ ssh -L 8888:localhost:8888 <username>@<remotehost>

今回は Vagrant の環境を使っているので、こんな感じで。

client $ vagrant ssh-config > ssh.config
client $ ssh -L 8888:localhost:8888 -F ssh.config default

あとは、クライアントのブラウザでローカルホストにマッピングしたポート番号を開く。

client $ open http://localhost:8888

すると、見覚えのある Web UI が表示される。 オプションの共通パスワード認証を使っていないのであれば、いきなりいつもの画面になるはず。

f:id:momijiame:20181014022447p:plain

あとは、もしポータビリティとかを考えるのであればお好みで Docker イメージとかにする感じで。

めでたしめでたし。