Provide utilities to automate secure secret key generation

Implements blueprint automatic-secure-key-generation

Reduce the likeliness that the (commented-out) default key is abused
and document possible options instead.

Also use a non-empty SECRET_KEY for development / testing environments.

A later patch would make it a hard error if no SECRET_KEY is defined
(i.e. Django defaults to an empty string which is anything but secure).
Unfortunately, I can't do it now as the devstack integration test would
fail (they don't set a SECRET_KEY either) currently. So, when this
blueprint is accepted, I would submit a fix to devstack and afterwards
add the error message to warn the user about insecure defaults.

Addressed PEP-8 issues

Change-Id: Ifdab8e6b6fb3025fde7a2b92beb046ec9c5cba7f
This commit is contained in:
Sascha Peilicke 2012-06-21 13:20:12 +02:00
parent 8e8d5a75d5
commit 9aa2dda073
6 changed files with 111 additions and 3 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ pylint.txt
reports reports
horizon.egg-info horizon.egg-info
openstack_dashboard/local/local_settings.py openstack_dashboard/local/local_settings.py
openstack_dashboard/test/.secret_key_store
doc/build/ doc/build/
doc/source/sourcecode doc/source/sourcecode
/static/ /static/

View File

@ -15,9 +15,12 @@
# under the License. # under the License.
import os
from horizon import test from horizon import test
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from horizon.utils import fields from horizon.utils import fields
from horizon.utils import secret_key
class ValidatorsTests(test.TestCase): class ValidatorsTests(test.TestCase):
@ -169,3 +172,24 @@ class ValidatorsTests(test.TestCase):
"169.144.11.107/8") "169.144.11.107/8")
self.assertIsNone(iprange.validate("fe80::204:61ff:254.157.241.86/36")) self.assertIsNone(iprange.validate("fe80::204:61ff:254.157.241.86/36"))
self.assertIsNone(iprange.validate("169.144.11.107/18")) self.assertIsNone(iprange.validate("169.144.11.107/18"))
class SecretKeyTests(test.TestCase):
def test_generate_secret_key(self):
key = secret_key.generate_key(32)
self.assertEqual(len(key), 32)
self.assertNotEqual(key, secret_key.generate_key(32))
def test_generate_or_read_key_from_file(self):
key_file = ".test_secret_key_store"
key = secret_key.generate_or_read_from_file(key_file)
# Consecutive reads should come from the already existing file:
self.assertEqual(key, secret_key.generate_or_read_from_file(key_file))
# Key file only be read/writable by user:
self.assertEqual(oct(os.stat(key_file).st_mode & 0777), "0600")
os.chmod(key_file, 0777)
self.assertRaises(secret_key.FilePermissionError,
secret_key.generate_or_read_from_file, key_file)
os.remove(key_file)

View File

@ -0,0 +1,68 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import with_statement # Python 2.5 compliance
import lockfile
import random
import string
import tempfile
import os
class FilePermissionError(Exception):
"""The key file permissions are insecure."""
pass
def generate_key(key_length=64):
"""Secret key generator.
The quality of randomness depends on operating system support,
see http://docs.python.org/library/random.html#random.SystemRandom.
"""
if hasattr(random, 'SystemRandom'):
choice = random.SystemRandom().choice
else:
choice = random.choice
return ''.join(map(lambda x: choice(string.digits + string.letters),
range(key_length)))
def generate_or_read_from_file(key_file='.secret_key', key_length=64):
"""Multiprocess-safe secret key file generator.
Useful to replace the default (and thus unsafe) SECRET_KEY in settings.py
upon first start. Save to use, i.e. when multiple Python interpreters
serve the dashboard Django application (e.g. in a mod_wsgi + daemonized
environment). Also checks if file permissions are set correctly and
throws an exception if not.
"""
lock = lockfile.FileLock(key_file)
with lock:
if not os.path.exists(key_file):
key = generate_key(key_length)
old_umask = os.umask(0177) # Use '0600' file permissions
with open(key_file, 'w') as f:
f.write(key)
os.umask(old_umask)
else:
if oct(os.stat(key_file).st_mode & 0777) != '0600':
raise FilePermissionError("Insecure key file permissions!")
with open(key_file, 'r') as f:
key = f.readline()
return key

View File

@ -12,9 +12,6 @@ TEMPLATE_DEBUG = DEBUG
# https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header # https://docs.djangoproject.com/en/1.4/ref/settings/#secure-proxy-ssl-header
# SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
# Note: You should change this value
SECRET_KEY = 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0'
# Specify a regular expression to validate user passwords. # Specify a regular expression to validate user passwords.
# HORIZON_CONFIG = { # HORIZON_CONFIG = {
# "password_validator": { # "password_validator": {
@ -25,6 +22,18 @@ SECRET_KEY = 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0'
LOCAL_PATH = os.path.dirname(os.path.abspath(__file__)) LOCAL_PATH = os.path.dirname(os.path.abspath(__file__))
# Set custom secret key:
# You can either set it to a specific value or you can let horizion generate a
# default secret key that is unique on this machine, e.i. regardless of the
# amount of Python WSGI workers (if used behind Apache+mod_wsgi): However, there
# may be situations where you would want to set this explicitly, e.g. when
# multiple dashboard instances are distributed on different machines (usually
# behind a load-balancer). Either you have to make sure that a session gets all
# requests routed to the same dashboard instance or you set the same SECRET_KEY
# for all of them.
# from horizon.utils import secret_key
# SECRET_KEY = secret_key.generate_or_read_from_file(os.path.join(LOCAL_PATH, '.secret_key_store'))
# We recommend you use memcached for development; otherwise after every reload # We recommend you use memcached for development; otherwise after every reload
# of the django development server, you will have to login again. To use # of the django development server, you will have to login again. To use
# memcached set CACHE_BACKED to something like 'memcached://127.0.0.1:11211/' # memcached set CACHE_BACKED to something like 'memcached://127.0.0.1:11211/'

View File

@ -1,10 +1,13 @@
import os import os
from horizon.tests.testsettings import * from horizon.tests.testsettings import *
from horizon.utils.secret_key import generate_or_read_from_file
TEST_DIR = os.path.dirname(os.path.abspath(__file__)) TEST_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, "..")) ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, ".."))
SECRET_KEY = generate_or_read_from_file(os.path.join(TEST_DIR,
'.secret_key_store'))
ROOT_URLCONF = 'openstack_dashboard.urls' ROOT_URLCONF = 'openstack_dashboard.urls'
TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),) TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),)
STATICFILES_DIRS = (os.path.join(ROOT_PATH, 'static'),) STATICFILES_DIRS = (os.path.join(ROOT_PATH, 'static'),)

View File

@ -6,3 +6,6 @@ python-glanceclient
python-keystoneclient python-keystoneclient
python-novaclient python-novaclient
pytz pytz
# Horizon Utility Requirements
lockfile # for SECURE_KEY generation