From 9ae708b36796582afd0b6c06db35a4b521e1b8ce Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 14 Feb 2017 14:36:50 +0100 Subject: [PATCH] use 32B hex cookie secret instead of large b64 secret, which doesn't make sense for sha256 Warn about deprecated base64 secrets and too-large secrets. --- docs/source/getting-started.md | 9 +++--- jupyterhub/app.py | 51 ++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/docs/source/getting-started.md b/docs/source/getting-started.md index e3e2902a..ee6d1c43 100644 --- a/docs/source/getting-started.md +++ b/docs/source/getting-started.md @@ -321,10 +321,11 @@ as follows: c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/cookie_secret' ``` -The content of this file should be a long random string encoded in MIME Base64. An example would be to generate this file as: +The content of this file should be 32 random bytes, encoded as hex. +An example would be to generate this file with: ```bash -openssl rand -base64 2048 > /srv/jupyterhub/cookie_secret +openssl rand -hex 32 > /srv/jupyterhub/cookie_secret ``` In most deployments of JupyterHub, you should point this to a secure location on the file @@ -339,7 +340,7 @@ the `JPY_COOKIE_SECRET` environment variable, which is a hex-encoded string. You can set it this way: ```bash -export JPY_COOKIE_SECRET=`openssl rand -hex 1024` +export JPY_COOKIE_SECRET=`openssl rand -hex 32` ``` For security reasons, this environment variable should only be visible to the Hub. @@ -350,7 +351,7 @@ You can also set the cookie secret in the configuration file itself,`jupyterhub_ as a binary string: ```python -c.JupyterHub.cookie_secret = bytes.fromhex('VERY LONG SECRET HEX STRING') +c.JupyterHub.cookie_secret = bytes.fromhex('64 CHAR HEX STRING') ``` ### Proxy authentication token diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 99e08ce4..405803f8 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -6,16 +6,18 @@ import atexit import binascii +from datetime import datetime +from getpass import getuser import logging import os +import re import shutil import signal import socket -import sys -import threading -from datetime import datetime -from getpass import getuser from subprocess import Popen +import sys +from textwrap import dedent +import threading from urllib.parse import urlparse if sys.version_info[:2] < (3, 3): @@ -36,7 +38,7 @@ from tornado import gen, web from traitlets import ( Unicode, Integer, Dict, TraitError, List, Bool, Any, Type, Set, Instance, Bytes, Float, - observe, default, + observe, default, validate, ) from traitlets.config import Application, catch_config_error @@ -99,8 +101,9 @@ flags = { ), } -SECRET_BYTES = 2048 # the number of bytes to use when generating new secrets +COOKIE_SECRET_BYTES = 32 # the number of bytes to use when generating new cookie secrets +HEX_RE = re.compile('([a-f0-9]{2})+', re.IGNORECASE) class NewToken(Application): """Generate and print a new API token""" @@ -409,11 +412,20 @@ class JupyterHub(Application): help="""The cookie secret to use to encrypt cookies. Loaded from the JPY_COOKIE_SECRET env variable by default. + + Should be exactly 256 bits (32 bytes). """ ).tag( config=True, env='JPY_COOKIE_SECRET', ) + @validate('cookie_secret') + def _cookie_secret_check(self, proposal): + secret = proposal.value + if len(secret) > COOKIE_SECRET_BYTES: + self.log.warning("Cookie secret is %i bytes. It should be %i.", + len(secret), COOKIE_SECRET_BYTES, + ) cookie_secret_file = Unicode('jupyterhub_cookie_secret', help="""File in which to store the cookie secret.""" @@ -736,25 +748,42 @@ class JupyterHub(Application): if perm & 0o07: raise ValueError("cookie_secret_file can be read or written by anybody") with open(secret_file) as f: - b64_secret = f.read() - secret = binascii.a2b_base64(b64_secret) + text_secret = f.read().strip() + if HEX_RE.match(text_secret): + # >= 0.8, use 32B hex + secret = binascii.a2b_hex(text_secret) + else: + # old b64 secret with a bunch of ignored bytes + secret = binascii.a2b_base64(text_secret) + self.log.warning(dedent(""" + Old base64 cookie-secret detected in {0}. + + JupyterHub >= 0.8 expects 32B hex-encoded cookie secret + for tornado's sha256 cookie signing. + + To generate a new secret: + + openssl rand -hex 32 > "{0}" + """).format(secret_file)) except Exception as e: self.log.error( "Refusing to run JupyterHub with invalid cookie_secret_file. " "%s error was: %s", secret_file, e) self.exit(1) + if not secret: secret_from = 'new' self.log.debug("Generating new %s", trait_name) - secret = os.urandom(SECRET_BYTES) + secret = os.urandom(COOKIE_SECRET_BYTES) if secret_file and secret_from == 'new': # if we generated a new secret, store it in the secret_file self.log.info("Writing %s to %s", trait_name, secret_file) - b64_secret = binascii.b2a_base64(secret).decode('ascii') + text_secret = binascii.b2a_hex(secret).decode('ascii') with open(secret_file, 'w') as f: - f.write(b64_secret) + f.write(text_secret) + f.write('\n') try: os.chmod(secret_file, 0o600) except OSError: