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
horizon.egg-info
openstack_dashboard/local/local_settings.py
openstack_dashboard/test/.secret_key_store
doc/build/
doc/source/sourcecode
/static/

View File

@ -15,9 +15,12 @@
# under the License.
import os
from horizon import test
from django.core.exceptions import ValidationError
from horizon.utils import fields
from horizon.utils import secret_key
class ValidatorsTests(test.TestCase):
@ -169,3 +172,24 @@ class ValidatorsTests(test.TestCase):
"169.144.11.107/8")
self.assertIsNone(iprange.validate("fe80::204:61ff:254.157.241.86/36"))
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
# 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.
# HORIZON_CONFIG = {
# "password_validator": {
@ -25,6 +22,18 @@ SECRET_KEY = 'elj1IWiLoWHgcyYxFVLj7cM5rGOOxWl0'
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
# 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/'

View File

@ -1,10 +1,13 @@
import os
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__))
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'
TEMPLATE_DIRS = (os.path.join(ROOT_PATH, 'templates'),)
STATICFILES_DIRS = (os.path.join(ROOT_PATH, 'static'),)

View File

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