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:
parent
8e8d5a75d5
commit
9aa2dda073
1
.gitignore
vendored
1
.gitignore
vendored
@ -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/
|
||||
|
@ -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)
|
||||
|
68
horizon/utils/secret_key.py
Normal file
68
horizon/utils/secret_key.py
Normal 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
|
@ -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/'
|
||||
|
@ -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'),)
|
||||
|
@ -6,3 +6,6 @@ python-glanceclient
|
||||
python-keystoneclient
|
||||
python-novaclient
|
||||
pytz
|
||||
|
||||
# Horizon Utility Requirements
|
||||
lockfile # for SECURE_KEY generation
|
||||
|
Loading…
Reference in New Issue
Block a user