JupyterHub est un logiciel libre distribué sous licence BSD qui a pour objectif de fournir une solution de développement multi-utilisateurs basée sur Jupyter à travers des “notesbook”, c’est-à-dire une application web qui juxtapose sur un seul document :
Cette interactivité permet aux élèves d’expérimenter et de tester leur code au fur à mesure de l’avancée dans leur document de travail, devrait faciliter l’apprentissage de la programmation en Python.
On trouve dans ce document quelques éléments permettant d’auto-héberger un serveur de de développement JupyterHub, c’est-à-dire :
jupyter.
d’un nom de domaine.Pour la suite, j’utilise les variables :
${JUPYTER} # le nom de la VM sur laquelle tourne JupyterHub
${PRIVATE_IP} # l'IP privée de cette VM, du type 192.168.1.XX
${PUBLIC_IP} # votre IP publique, celle de votre box
${MONDOMAINE} # votre nom de domaine
J’ai pris l’habitude d’administrer mes machines virtuelles avec KVM. Pour faire de même, sur l’hôte installer :
# apt install qemu-system qemu-system-x86 qemu-utils \
libvirt-clients libvirt-daemon-system \
virtinst bridge-utils
Pour que la VM partage la connexion réseau de l’hôte, il est nécessaire de définir une connexion de type bridge par exemple avec le script :
export MAIN_CONN=enp0s31f6
bash -x << EOF
systemctl stop libvirtd
nmcli c delete "$MAIN_CONN"
nmcli c delete "Wired connection 1"
nmcli c add type bridge ifname br0 autoconnect yes con-name br0 stp off
nmcli c modify br0 ipv4.addresses ${PRIVATE_IP}/24 ipv4.method manual
nmcli c modify br0 ipv4.gateway 192.168.1.1
nmcli c modify br0 ipv4.dns 192.168.1.1
nmcli c add type bridge-slave autoconnect yes con-name "$MAIN_CONN" \
ifname "$MAIN_CONN" master br0
systemctl restart NetworkManager
systemctl start libvirtd
systemctl enable libvirtd
echo "net.ipv4.ip_forward = 1" \
| sudo tee /etc/sysctl.d/99-ipforward.conf
sysctl -p /etc/sysctl.d/99-ipforward.conf
EOF
où enp0s31f6
est le nom de votre carte réseau, obtenu avec :
$ nmcli con show
Sur une machine « correcte », créer une machine virtuelle, ici avec Debian bullseye (on ne se refait pas) à laquelle j’ai alloué 4 coeurs (vcpus
) et 8 Go de RAM (ram
) :
# virt-install \
--name ${JUPYTER} \ --vcpus 4 \
--ram 8192 \
--disk path=/somewhere/${JUPYTER}.qcow2,size=20 \
--os-type linux \
--os-variant debian11 \
--network bridge=br0 \
--graphics none \
--console pty,target_type=serial \
--location 'http://ftp.debian.org/debian/dists/bullseye/main/installer-amd64/' \
--extra-args 'console=ttyS0,115200n8 serial'
où br0
est l’interface bridge.
procéder à l’installation avec juste un serveur SSH et le minimum vital des coreutils ;
démarrer et se connecter à la machine virtuelle
# virsh start ${JUPYTER}
# virsh console ${JUPYTER}
changer le nom d’hôte
# hostnamectl set-hostname jupyter
mettre à jour puis installer les principaux paquets
# apt update && apt full-upgrade
# apt install vim npm nodejs python3 python3-pip \
python3-numpy python3-matplotlib python3-notebook
# npm install -g configurable-http-proxy
# pip3 install --upgrade pip
# pip3 install jupyterhub
générer le cookie secret et le jeton d’authentification
# mkdir -p /srv/jupyterhub && cd $_
# touch jupyterhub_cookie_secret
# chown :sudo jupyterhub_cookie_secret
# chmod g+rw jupyterhub_cookie_secret
# openssl rand -hex 32 > jupyterhub_cookie_secret
# touch proxy_auth_token
# chown :sudo proxy_auth_token
# chmod g+rw proxy_auth_token
# openssl rand -hex 32 > proxy_auth_token
éditer la configuration
# jupyterhub --generate-config
# vim jupyterhub_config.py
avec les lignes
c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret'
c.ConfigurableHTTPProxy.auth_token = '/srv/jupyterhub/proxy_auth_token'
c.Authenticator.whitelist = {'<user>'}
c.Authenticator.admin_users = {'<user>'}
copier le fichier de config
# cp jupyterhub_config.py /srv/jupyterhub/.
lancez jupyterhub
depuis votre réseau local
# ./jupyterhub
puis vérifier que vous pouvez vous connecter à l’adresse
http://<localIP>:8000
Si vous êtes derrière une box Internet, ouvrez les ports du pare-feu et redirigez les vers votre serveur frontal (sur les mêmes ports) :
Dans le cas où JupyterHub n’est pas le seul service hébergé sur ${MONDOMAINE}
, il est nécessaire de définir un reverse proxy pour rediriger les requêtes de votre sous-domaine
jupyter.${MONDOMAINE}
vers
http://${PRIVATE_IP}:8000
Pour le reverse proxy, on utilise ici NGINX.
Installer NGINX
# apt install nginx
démarrer le service
# systemctl start nginx
puis le rendre actif à chaque démarrage
# systemctl enable nginx
Afin de sécuriser le protocole HTTP avec une couche de chiffrement SSL/TLS et garder privées les communications entre les utilisateurs et le serveur, on utilisera par défaut le HTTPS, en écoute du port TCP 443.
On se réfère aux recommandations de la fondation mozilla qui ne sont pas en contradiction avec celles de l’ANSSI. On utilise en particulier le générateur sur lequel on renseigne la version de [NGINX], obtenue avec :
$ nginx -v
et celle de openssl
$ openssl version
Pour intégrer ces recommandations, il faut :
générer dhparam.pem
nécessaire à la configuration SSL avec [NGINX]
# openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
éditer le fichier /etc/nginx/nginx.conf
pour
TLSv1
et TLSv1.1
sur la ligne ssl_protocols
pour ne garder que TLSv1.2
et TLSv1.3
;ssl_prefer_server_ciphers
pour off
;et donc avoir les lignes :
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
relancer [NGINX]
# systemctl reload nginx
# service nginx restart
On se base sur les recommandations de l’ANSII et sur l’article pour durcir les réponses HTTP du serveur, notamment :
iframes
;Pour cela, éditer le fichier de configuration :
# vim /etc/nginx/nginx.conf
en ajoutant les lignes :
server{
...
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Xss-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header 'Referrer-Policy' 'origin';
...
}
puis relancer [NGINX] :
# service nginx restart
Vérifier ensuite votre configuration en faisant analyser votre site.
créer un fichier de configuration pour le service associé à JupyterHub
# cat > /etc/nginx/conf.d/jupyter.conf << EOF
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name jupyter.${MONDOMAINE};
# Tell all requests to port 80 to be 302 redirected to HTTPS
return 302 https://$host$request_uri;
}
server {
listen 443;
ssl on;
server_name jupyter.${MONDOMAINE};
ssl_certificate /etc/letsencrypt/live/jupyter.${MONDOMAINE}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jupyter.${MONDOMAINE}/privkey.pem;
# Managing literal requests to the JupyterHub front end
location / {
proxy_pass http://${PRIVATE_IP}: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;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
# Managing requests to verify letsencrypt host
location ~ /.well-known {
allow all;
}
EOF
Activer le pare-feu
# apt install ufw
# ufw enable
puis ouvrir les ports
# ufw allow ssh
# ufw allow 80/tcp
# ufw allow 443/tcp
# ufw allow 8000/tcp
# ufw allow 8000/udp
puis vérifier
# ufw status
qui doit renvoyer
Status: active
To Action From
-- ------ ----
Nginx HTTP ALLOW Anywhere
Nginx HTTPS ALLOW Anywhere
22/tcp ALLOW Anywhere
8000 ALLOW Anywhere
Nginx HTTP (v6) ALLOW Anywhere (v6)
Nginx HTTPS (v6) ALLOW Anywhere (v6)
22/tcp (v6) ALLOW Anywhere (v6)
8000 (v6) ALLOW Anywhere (v6)
arrêter le serveur nginx
# service nginx stop
générer les certificats SSL
# apt install certbot
# certbot certonly --standalone -d jupyter.${MONDOMAINE}
qui va générer deux fichiers :
/etc/letsencrypt/live/jupyter.${MONDOMAINE}/fullchain.pem
/etc/letsencrypt/live/jupyter.${MONDOMAINE}/privkey.pem
puis générer dhparam.pem
nécessaire à la configuration SSL avec nginx
# openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
relancer nginx
# systemctl reload nginx
# service nginx restart
Relancer JupyterHub avec l’option :
# ./jupyterhub
Connectez-vous de l’extérieur avec l’URL
https://jupyter.${MONDOMAINE}
vous devez maintenant avoir une connexion sécurisée.
Pour définir un service qui permette de lancer JupyterHub avec systemd
dès le lancement de la VM :
virsh start ${JUPYTER}
éditer le fichier de service
# cat > /srv/jupyterhub/jupyterhub.service << EOF
[Unit]
Description=JupyterHub
After=syslog.target network.target
[Service]
User=root
Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/usr/local/bin/jupyterhub -f /srv/jupyterhub/jupyterhub_config.py
[Install]
WantedBy=multi-user.target
EOF
faire un lien symbolique vers systemd
# ln -s /srv/jupyterhub/jupyterhub.service /etc/systemd/system/.
relancer le daemon
# systemctl daemon-reload
activer puis démarrer le service
# systemctl enable jupyterhub.service
# systemctl start jupyterhub.service
et vérifier son statut avec
# systemctl status jupyterhub.service
Pour mettre à jour JupyterHub, il suffit d’utiliser le gestionaire de paquets du système
# apt update && apt full-upgrade
et avec pip3
:
# pip3 install --upgrade jupyterhub
En cas de problème avec la mise à jour de la base de données, quand la commande
# jupyterhub upgrade-db
n’a aucun effet, il faut la supprimer pour en créer une nouvelle
# find / -name "jupyterhub.sqlite" -delete