This project shows howto authenticate jupyter notebook users using SAML. Upon successful authentication, a private notebook is spawned as a docker container.
JupyterHub-SAML
What do we need:
- docker
- docker-compose
- domain name
- SAML Preparations
- install + configure our hosts
- prepare a notebook
docker
Please make sure you have docker up and running on your system.~~~ https://docs.docker.com/get-started/ ~~~
docker-compose
Please make sure also docker-compose is installed on your system.~~~ https://docs.docker.com/compose/install/ ~~~
domain name
For this demonstration we like to have a domainname that we can reach on the public internet. If you have such a domainname already and you do have control over the DNS settings of that domain, then the recommendation is to reserve a subdomain and register a A-record to the IP-Address of your Docker VPS/Machine. Make sure that port 443 is open on the firewall.SAML Preparations
Here we need several steps.If you are new to SAML and Federated Authentication, here is some good readings that will get you prepared with the required background information.
~~~ https://wiki.surfnet.nl/display/surfconextdev/Schematic+overview ~~~
Our VPS machine will act as a "Service Provider". During this demonstration, we make use of surfconext to connect to our "Identity Providers".
In preparation for that we need the following:
Service Provider key-set
The following commands will generate a self-signed keyset that is OK for this demonstration.
~~~ openssl genrsa -out server.key openssl req -new -x509 -key server.key -out server.crt -days 365 ~~~
Service Provide Metadata
We need to prepare Metadata that we can pass on to the Identity Provider in order to establish a bilateral "trust-relation" between us.
This template can be used and adjusted where appropriate.
File: metadata.xml
~~~
Please replace at least these placeholders with the appropriate values:
| Placeholder | To be replaced by | | --- | --- | | %%% DOMAIN %%% | the full domain name, for example: https://www.example.org | | %%% X509 %%% | output of command openssl x509 -in server.crt | |(please remove the lines BEGIN CERTICATE and END CERTIFICATE). | | %%% SERVICE NAME %%% | The Service Name describing your service |
Before this Metadata can be send to the Identity Provider, we need to sign the contents.
This can be achieved from command line by using 'xmlsec1'.
~~~ xmlsec1 --sign --output signed_metadata.xml --privkey-pem server.key metadata.xml ~~~
Install + configure our hosts
As presented in the diagram earlier, we have 3 host components.
- proxy
- saml
- jupyter
The componens are specified in our docker-compose file: docker-compose.yml.
~~~ version: '2' services:
proxy: hostname: ${MY_HOSTNAME} image: nginx:alpine networks: - front-end - back-end ports: - "443:443" - "80:80" volumes: - $PWD/etc/letsencrypt:/etc/letsencrypt:ro - $PWD/etc/nginx.template:/etc/nginx/nginx.template:ro - $PWD/img:/www/data/img:ro restart: always command: 'sh -c "cat /etc/nginx/nginx.template | sed \"s/MYDOMAINNAME/${MY_HOSTNAME}/\" > /etc/nginx/nginx.conf && nginx -g \"daemon off;\""'
saml: hostname: ${MY_HOSTNAME} build: context: saml dockerfile: Dockerfile networks: - back-end ports: - "443" environment: SERVERNAME: ${MYHOSTNAME} SHIBBOLETHSPENTITYID: ${MYENTITY_ID} SHIBBOLETHSPCERT: /run/sp/sp-cert.pem SHIBBOLETHSPPRIVKEY: /run/sp/sp-key.pem SHIBBOLETHSPMETADATAPROVIDERXML_FILE: /run/sp/sp-metadata-myvelocity.xml volumes: - $PWD/etc/sp:/run/sp:ro - $PWD/etc/sp/myvelocity-shibboleth2.xml:/etc/shibboleth/shibboleth2.xml:ro - $PWD/etc/sp/attribute-map.xml:/etc/shibboleth/attribute-map.xml:ro - $PWD/etc/letsencrypt:/etc/letsencrypt:ro - $PWD/etc/idp/surfconext.test/certificate.pem:/opt/shibboleth-sp/etc/shibboleth/surfconext.pem:ro
jupyter: build: context: $PWD/jupyterhub dockerfile: Dockerfile.jupyterhub args: - JUPYTERHUBVERSION=${MYJUPYTERHUB_VERSION} networks: - back-end ports: - "8000" volumes: - "/var/run/docker.sock:/var/run/docker.sock:rw" - $PWD/etc/jupyterhub:/srv/jupyterhub - $PWD/var/jupyter:/volumes/jupyter command: jupyterhub
networks: front-end: driver: bridge back-end: driver: bridge ~~~
Special attenticon for the network specification at the bottom of this file. The "back-end" network is relevant for the juputer interaction between the notebook and the hub. Later we will see that the the network name is specified within the JupyterHub-Configuration file.
The environment variables used in the docker-compose.yml file can be provided using a .env
This file should contain:
~~~ MY_HOSTNAME=www.yourdomain.com MYENTITYID=https://%%% SERVICE NAME %%%/metadata MYJUPYTERHUBVERSION=0.8.0 MYDOCKERNOTEBOOK_IMAGE=jupyter/scipy-notebook MYLOCALNOTEBOOK_IMAGE=jupyterhub-user ~~~
Note: The value of MY\ENTITY\ID must match the value that you have provided in your METADATA at the attribute: entityID
Configure NGINX - Reverse Proxy
The NGINX Proxy functions as our single internet connected host. The proxy takes care of SSL-offloading and passing the requests downstream to the other components.
The following proxying takes place:
- static contents like images are served directly.
- all requests to /jupyter and /hub and forwarded to be handled by the SAML host. The SAML host enforces authenticated session before additional services can be offered to the user.
- all request to /user are passed on to the Jupyter host. This host will forward request to the appropriate runnint notebook but only if there is an active valid session for the user.
- Jupyter User Notebooks requests are directly passed onto the JupyterHUB and may bypass the SAML host.
- All /saml requests are forwarded ot the SAML host.
~~~ location /img { root /www/data; }
location /jupyter { proxy_pass https://saml/jupyter; }
location /hub { proxy_pass https://saml/jupyter/hub; }
location /user { proxy_pass http://jupyter:8000/user; }
location /saml { proxy_pass https://saml/saml; } ~~~
Configure SAML (Apache + Shibboleth)
This host is serving a standard Apache2 webserver as well as a Shibboleth Server. Please refer to the saml/Dockerfile for the details on how this image is build.
The relevant part in the Apache Configuration takes care of the SAML handling and a value of REMOTE_USER is set after succesful authentication.
~~~
RewriteEngine On RewriteCond %{LA-U:REMOTE_USER} (.*) RewriteRule . - [E=RU:%1] RequestHeader set REMOTE_USER "%{RU}e" env=RU
ProxyPreserveHost On
ProxyPass /jupyter http://jupyter:8000/ ProxyPassReverse /jupyter http://jupyter:8000/
ProxyPass /jupyter/hub http://jupyter:8000/hub ProxyPassReverse /jupyter/hub http://jupyter:8000/hub ~~~
Prepare Jupyter
The Jupyter host is prepared from a standard JupyterHub docker image with added support for DockerSpawner and Remote User Authentication.
The Docker build file looks like:
~~~
Copyright (c) Jupyter Development Team.
Distributed under the terms of the Modified BSD License.
ARG JUPYTERHUB_VERSION FROM jupyterhub/jupyterhub-onbuild:$JUPYTERHUB_VERSIONInstall dockerspawner, oauth, postgres
RUN /opt/conda/bin/conda install -yq psycopg2=2.7 && \ /opt/conda/bin/conda clean -tipsy && \ /opt/conda/bin/pip install --no-cache-dir \ jhubremoteuser_authenticator==0.0.* \ dockerspawner==0.9.*~~~
The Jupyter host acts like a hub. The configuration is specified in etc/jupyterhub/jupyterhub_config.py
Some important details are:
| Variable | Value | | --- | --- | | DOCKER\NOTEBOOK\IMAGE | "jupyterhub-user" | | c.JupyterHub.spawner_class | 'dockerspawner.DockerSpawner' | | networkname | 'jupyterhubsamlback-end' |
In order to allow volume names in our notebook to be created with some special characters (like '@' in email names), we need to address the proper volume naming plugin.
~~~ import dockerspawner c.DockerSpawner.formatvolumename = dockerspawner.volumenamingstrategy.escapedformatvolume_name ~~~
Here we instantiate the Remote User authenticator as our JupyterHub authenticator.
~~~ c.JupyterHub.authenticatorclass = 'jhubremoteuserauthenticator.remoteuserauth.RemoteUserAuthenticator' ~~~
Prepare a notebook
The notebook is build by a seperate Makefile and results in a Docker Image with the tag-name jupyterhub-user
Command to (re-)build your notebook
~~~ cd notebook make ~~~