Compare commits

...

35 Commits
0.1.4 ... 0.1.5

Author SHA1 Message Date
56f7da34d7 Merge pull request #129 from kevgliss/version
Version bump and needed documentation.
2015-10-26 11:59:16 -07:00
0f34440b64 Merge pull request #130 from kevgliss/createUser
Fixing issue where roles were not added correctly to user.
2015-10-26 11:58:36 -07:00
bbcc7cca4e Merge pull request #128 from kevgliss/fernet_migration
[WIP] Adding aes - fernet migration
2015-10-26 10:59:41 -07:00
0453afcb0e Fixing issuer where roles were not added correctly to user. 2015-10-26 10:59:20 -07:00
cafecd1e19 Version bump and needed documentation. 2015-10-24 11:18:27 -07:00
4b968a9474 Adding aes - fernet migration 2015-10-23 16:47:17 -07:00
9244945e69 Merge pull request #127 from monkeysecurity/update_cryptography_for_el_capitan
Updating cryptography to 1.0.2 for el capitan
2015-10-21 18:54:43 -07:00
78819c1733 Updating cryptography to 1.0.2 for el capitan 2015-10-21 18:45:50 -07:00
394e18f76e Merge pull request #123 from rpicard/master
Use MultiFernet for encryption
2015-10-14 15:50:33 -07:00
40eb950e94 Use MultiFernet for encryption
Facilitates key rotation and uses more secure encryption than what
sqlalchemy-utils does.

Fixes #117 and #119.
2015-10-13 16:58:58 -07:00
90636a5329 Merge pull request #118 from rpicard/master
Fix a handful of typos in documentation
2015-10-06 15:30:09 -07:00
2fc6d4cd21 Fix a handful of typos in documentation
As I was reading through the docs I made note of grammar issues and
typos I saw. Not a huge deal but might as well fix what I noticed.
2015-10-06 15:05:05 -07:00
b20bdf3c4e Merge pull request #116 from kevgliss/algo
Adding the ability to track a certificates signing key algorithm
2015-10-06 13:25:34 -07:00
a20726a301 Fixing python 3.x syntax error 2015-10-06 13:11:24 -07:00
39727a1c9f Fixing tests 2015-10-06 13:00:06 -07:00
168f46a436 Adding the ability to track a certificates signing key algorithm 2015-10-06 12:51:59 -07:00
4ec07a6dc7 Merge pull request #115 from kevgliss/destinations
Fixes destination stat
2015-10-06 09:48:03 -07:00
798a6295ee Fixes destination stat 2015-10-06 09:43:31 -07:00
73cb8da8c1 Merge pull request #114 from kevgliss/clipboard
Add clipboard functionality
2015-10-05 16:14:16 -07:00
3167ce9785 removing unneeded dep 2015-10-05 16:08:12 -07:00
63b7b71b49 adding clipboard functionality 2015-10-05 16:06:56 -07:00
9965af9ccd fixing links, and adding zeroclipboard 2015-10-05 09:48:52 -07:00
ba5d2c925a Merge pull request #113 from kevgliss/perma
Adding ui router and perma links to certificates and authorities
2015-10-05 09:30:52 -07:00
867be09e29 more double quotes 2015-10-05 09:24:11 -07:00
8362a92898 fixing double quotes 2015-10-05 09:19:14 -07:00
162482dbc4 Adding ui router and perma links to certificates and authorities 2015-10-05 09:00:51 -07:00
c0f14db5bb Merge pull request #112 from kevgliss/uitweaks
UI tweaks
2015-10-02 16:31:50 -07:00
3c561914c6 removing angular tour 2015-10-02 16:29:59 -07:00
34c6f1bf4d Merge pull request #111 from kevgliss/105
Closes #105
2015-10-02 16:26:02 -07:00
2187898494 adding copy and a better profile picture for non-sso users 2015-10-02 15:36:50 -07:00
d4bc6ae7a1 Fixes #105 2015-10-02 13:46:13 -07:00
81cdb15353 Merge pull request #108 from kevgliss/description
description should be optional
2015-09-29 16:56:58 -07:00
5cfa9d4bc5 description should be optional 2015-09-29 16:37:32 -07:00
92da453233 Merge pull request #107 from waffle-iron/master
waffle.io Badge
2015-09-29 13:39:36 -07:00
2aedfedbd3 add waffle.io badge 2015-09-29 13:50:33 -06:00
48 changed files with 924 additions and 556 deletions

View File

@ -1,2 +1,3 @@
- Kevin Glisson <kglisson@netflix.com>
- Jeremy Heffner <jheffner@netflix.com>

15
CHANGELOG.rst Normal file
View File

@ -0,0 +1,15 @@
Changelog
=========
0.2.0 - `master` _
~~~~~~~~~~~~~~~~~~~
.. note:: This version not yet released and is under active development
0.1.5 - 2015-10-26
~~~~~~~~~~~~~~~~~~~
* **SECURITY ISSUE**: Switched from use a AES static key to Fernet encryption.
Affects all versions prior to 0.1.5. If upgrading this will require a data migration.
see: `Upgrading Lemur <https://lemur.readthedocs.com/adminstration#UpgradingLemur>`_

View File

View File

@ -16,12 +16,16 @@ Lemur
.. image:: https://travis-ci.org/Netflix/lemur.svg
:target: https://travis-ci.org/Netflix/lemur
.. image:: https://badge.waffle.io/Netflix/lemur.png?label=ready&title=Ready
:target: https://waffle.io/Netflix/lemur
:alt: 'Stories in Ready'
Lemur manages TLS certificate creation. While not able to issue certificates itself, Lemur acts as a broker between CAs
and environments providing a central portal for developers to issue TLS certificates with 'sane' defaults.
It works on CPython 2.7, 3.3, 3.4. We deploy on Ubuntu and develop on OS X.
Project resources
=================

View File

@ -29,11 +29,15 @@
"angular-ui-switch": "~0.1.0",
"angular-chart.js": "~0.7.1",
"satellizer": "~0.9.4",
"angularjs-toaster": "~0.4.14"
"angularjs-toaster": "~0.4.14",
"ngletteravatar": "~3.0.1",
"angular-ui-router": "~0.2.15",
"angular-clipboard": "~1.1.1"
},
"devDependencies": {
"angular-mocks": "~1.3",
"angular-scenario": "~1.3"
"angular-scenario": "~1.3",
"ngletteravatar": "~3.0.1"
},
"resolutions": {
"bootstrap": "~3.3.1",

View File

@ -72,7 +72,7 @@ Basic Configuration
.. data:: LEMUR_TOKEN_SECRET
:noindex:
The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and be kept private.
The TOKEN_SECRET is the secret used to create JWT tokens that are given out to users. This should be securely generated and kept private.
::
@ -87,17 +87,23 @@ Basic Configuration
>>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6))
.. data:: LEMUR_ENCRYPTION_KEY
.. data:: LEMUR_ENCRYPTION_KEYS
:noindex:
The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse
to start.
The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
to start. Multiple keys can be provided to facilitate key rotation. The first key in the list is used for
encryption and all keys are tried for decryption until one works. Each key must be 32 URL safe base-64 encoded bytes.
See `LEMUR_TOKEN_SECRET` for methods of secure secret generation.
Running lemur create_config will securely generate a key for your configuration file.
If you would like to generate your own, we recommend the following method:
>>> import os
>>> import base64
>>> base64.urlsafe_b64encode(os.urandom(32))
::
LEMUR_ENCRYPTION_KEY = 'supersupersecret'
LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s=']
Certificate Default Options
@ -210,13 +216,13 @@ Authority Options
-----------------
Authorities will each have their own configuration options. There is currently just one plugin bundled with Lemur,
Verisign/Symantec. Additional plugins may define additional options. Refer to the plugins own documentation
Verisign/Symantec. Additional plugins may define additional options. Refer to the plugin's own documentation
for those plugins.
.. data:: VERISIGN_URL
:noindex:
This is the url for the verisign API
This is the url for the Verisign API
.. data:: VERISIGN_PEM_PATH
@ -256,7 +262,7 @@ for those plugins.
Authentication
--------------
Lemur currently supports Basic Authentication and Ping OAuth2 out of the box, additional flows can be added relatively easily.
Lemur currently supports Basic Authentication and Ping OAuth2 out of the box. Additional flows can be added relatively easily.
If you are not using Ping you do not need to configure any of these options.
For more information about how to use social logins, see: `Satellizer <https://github.com/sahat/satellizer>`_
@ -295,7 +301,7 @@ For more information about how to use social logins, see: `Satellizer <https://g
AWS Plugin Configuration
========================
In order for Lemur to manage it's own account and other accounts we must ensure it has the correct AWS permissions.
In order for Lemur to manage its own account and other accounts we must ensure it has the correct AWS permissions.
.. note:: AWS usage is completely optional. Lemur can upload, find and manage TLS certificates in AWS. But is not required to do so.
@ -348,9 +354,9 @@ STS-AssumeRole
Next we will create the the Lemur IAM role. Lemur
Next we will create the the Lemur IAM role.
..note::
.. note::
The default IAM role that Lemur assumes into is called `Lemur`, if you need to change this ensure you set `LEMUR_INSTANCE_PROFILE` to your role name in the configuration.
@ -470,7 +476,7 @@ The configuration::
LEMUR_MAIL = 'lemur.example.com'
Will be sender of all notifications, so ensure that it is verified with AWS.
Will be the sender of all notifications, so ensure that it is verified with AWS.
SES if the default notification gateway and will be used unless SMTP settings are configured in the application configuration
settings.
@ -554,9 +560,9 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
.. data:: check_revoked
Traverses every certificate that Lemur is aware of and attempts to understand it's validity.
Traverses every certificate that Lemur is aware of and attempts to understand its validity.
It utilizes both OCSP and CRL. If Lemur is unable to come to a conclusion about a certificates
validity it's status is marked 'unknown'
validity its status is marked 'unknown'
.. data:: sync
@ -607,10 +613,10 @@ meaning.
Within Lemur there are three main permissions: AdminPermission, CreatorPermission, OwnerPermission. Sub-permissions such
as ViewPrivateKeyPermission are compositions of these three main Permissions.
Lets take a look at how these permissions used:
Lets take a look at how these permissions are used:
Each `Authority` has a set of roles associated with it. If a user is also associated with the same roles
that the `Authority` is associated with it Lemur allows that user to user/view/update that `Authority`.
that the `Authority` is associated with, Lemur allows that user to user/view/update that `Authority`.
This RBAC is also used when determining which users can access which certificate private key. Lemur's current permission
structure is setup such that if the user is a `Creator` or `Owner` of a given certificate they are allow to view that
@ -621,3 +627,34 @@ These permissions are applied to the user upon login and refreshed on every requ
.. seealso::
`Flask-Principal <https://pythonhosted.org/Flask-Principal>`_
Upgrading Lemur
===============
To upgrade Lemur to the newest release you will need to ensure you have the lastest code and have run any needed
database migrations.
To get the latest code from github run
::
cd <lemur-source-directory>
git pull -t <version>
python setup.py develop
.. note::
It's important to grab the latest release by specifying the release tag. This tags denote stable versions of Lemur.
If you want to try the bleeding edge version of Lemur you can by using the master branch.
After you have the latest version of the Lemur code base you must run any needed database migrations. To run migrations
::
cd <lemur-source-directory>/lemur
lemur db upgrade
This will ensure that any needed tables or columns are created or destroyed.

View File

@ -1,2 +1 @@
Change Log
==========
.. include:: ../CHANGELOG.rst

View File

@ -4,8 +4,8 @@ Frequently Asked Questions
Common Problems
---------------
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'*
You likely have not correctly configured **LEMUR_ENCRYPTION_KEY**. See
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEYS set?'*
You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
:doc:`administration/index` for more information.
@ -14,6 +14,22 @@ I am seeing Lemur's javascript load in my browser but not the CSS.
:doc:`production/index` for example configurations.
Running 'lemur db upgrade' seems stuck.
Most likely, the upgrade is stuck because an existing query on the database is holding onto a lock that the
migration needs.
To resolve, login to your lemur database and run:
SELECT * FROM pg_locks l INNER JOIN pg_stat_activity s ON (l.pid = s.pid) WHERE waiting AND NOT granted;
This will give you a list of queries that are currently waiting to be executed. From there attempt to idenity the PID
of the query blocking the migration. Once found execute:
select pg_terminate_backend(<blocking-pid>);
See `<http://stackoverflow.com/questions/22896496/alembic-migration-stuck-with-postgresql>`_ for more.
How do I
--------

View File

@ -1,261 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
sphinx-autopackage-script
This script parses a directory tree looking for python modules and packages and
creates ReST files appropriately to create code documentation with Sphinx.
It also creates a modules index (named modules.<suffix>).
"""
# Copyright 2008 Société des arts technologiques (SAT), http://www.sat.qc.ca/
# Copyright 2010 Thomas Waldmann <tw AT waldmann-edv DOT de>
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import optparse
# automodule options
OPTIONS = ['members',
'undoc-members',
# 'inherited-members', # disabled because there's a bug in sphinx
'show-inheritance',
]
INIT = '__init__.py'
def makename(package, module):
"""Join package and module with a dot."""
# Both package and module can be None/empty.
if package:
name = package
if module:
name += '.' + module
else:
name = module
return name
def write_file(name, text, opts):
"""Write the output file for module/package <name>."""
if opts.dryrun:
return
fname = os.path.join(opts.destdir, "%s.%s" % (name, opts.suffix))
if not opts.force and os.path.isfile(fname):
print 'File %s already exists, skipping.' % fname
else:
print 'Creating file %s.' % fname
f = open(fname, 'w')
f.write(text)
f.close()
def format_heading(level, text):
"""Create a heading of <level> [1, 2 or 3 supported]."""
underlining = ['=', '-', '~', ][level-1] * len(text)
return '%s\n%s\n\n' % (text, underlining)
def format_directive(module, package=None):
"""Create the automodule directive and add the options."""
directive = '.. automodule:: %s\n' % makename(package, module)
for option in OPTIONS:
directive += ' :%s:\n' % option
return directive
def create_module_file(package, module, opts):
"""Build the text of the file and write the file."""
text = format_heading(1, '%s Module' % module)
text += format_heading(2, ':mod:`%s` Module' % module)
text += format_directive(module, package)
write_file(makename(package, module), text, opts)
def create_package_file(root, master_package, subroot, py_files, opts, subs):
"""Build the text of the file and write the file."""
package = os.path.split(root)[-1]
text = format_heading(1, '%s Package' % package)
# add each package's module
for py_file in py_files:
if shall_skip(os.path.join(root, py_file)):
continue
is_package = py_file == INIT
py_file = os.path.splitext(py_file)[0]
py_path = makename(subroot, py_file)
if is_package:
heading = ':mod:`%s` Package' % package
else:
heading = ':mod:`%s` Module' % py_file
text += format_heading(2, heading)
text += format_directive(is_package and subroot or py_path, master_package)
text += '\n'
# build a list of directories that are packages (they contain an INIT file)
subs = [sub for sub in subs if os.path.isfile(os.path.join(root, sub, INIT))]
# if there are some package directories, add a TOC for theses subpackages
if subs:
text += format_heading(2, 'Subpackages')
text += '.. toctree::\n\n'
for sub in subs:
text += ' %s.%s\n' % (makename(master_package, subroot), sub)
text += '\n'
write_file(makename(master_package, subroot), text, opts)
def create_modules_toc_file(master_package, modules, opts, name='modules'):
"""
Create the module's index.
"""
text = format_heading(1, '%s Modules' % opts.header)
text += '.. toctree::\n'
text += ' :maxdepth: %s\n\n' % opts.maxdepth
modules.sort()
prev_module = ''
for module in modules:
# look if the module is a subpackage and, if yes, ignore it
if module.startswith(prev_module + '.'):
continue
prev_module = module
text += ' %s\n' % module
write_file(name, text, opts)
def shall_skip(module):
"""
Check if we want to skip this module.
"""
# skip it, if there is nothing (or just \n or \r\n) in the file
return os.path.getsize(module) < 3
def recurse_tree(path, excludes, opts):
"""
Look for every file in the directory tree and create the corresponding
ReST files.
"""
# use absolute path for root, as relative paths like '../../foo' cause
# 'if "/." in root ...' to filter out *all* modules otherwise
path = os.path.abspath(path)
# check if the base directory is a package and get is name
if INIT in os.listdir(path):
package_name = path.split(os.path.sep)[-1]
else:
package_name = None
toc = []
tree = os.walk(path, False)
for root, subs, files in tree:
# keep only the Python script files
py_files = sorted([f for f in files if os.path.splitext(f)[1] == '.py'])
if INIT in py_files:
py_files.remove(INIT)
py_files.insert(0, INIT)
# remove hidden ('.') and private ('_') directories
subs = sorted([sub for sub in subs if sub[0] not in ['.', '_']])
# check if there are valid files to process
# TODO: could add check for windows hidden files
if "/." in root or "/_" in root \
or not py_files \
or is_excluded(root, excludes):
continue
if INIT in py_files:
# we are in package ...
if (# ... with subpackage(s)
subs
or
# ... with some module(s)
len(py_files) > 1
or
# ... with a not-to-be-skipped INIT file
not shall_skip(os.path.join(root, INIT))
):
subroot = root[len(path):].lstrip(os.path.sep).replace(os.path.sep, '.')
create_package_file(root, package_name, subroot, py_files, opts, subs)
toc.append(makename(package_name, subroot))
elif root == path:
# if we are at the root level, we don't require it to be a package
for py_file in py_files:
if not shall_skip(os.path.join(path, py_file)):
module = os.path.splitext(py_file)[0]
create_module_file(package_name, module, opts)
toc.append(makename(package_name, module))
# create the module's index
if not opts.notoc:
create_modules_toc_file(package_name, toc, opts)
def normalize_excludes(rootpath, excludes):
"""
Normalize the excluded directory list:
* must be either an absolute path or start with rootpath,
* otherwise it is joined with rootpath
* with trailing slash
"""
sep = os.path.sep
f_excludes = []
for exclude in excludes:
if not os.path.isabs(exclude) and not exclude.startswith(rootpath):
exclude = os.path.join(rootpath, exclude)
if not exclude.endswith(sep):
exclude += sep
f_excludes.append(exclude)
return f_excludes
def is_excluded(root, excludes):
"""
Check if the directory is in the exclude list.
Note: by having trailing slashes, we avoid common prefix issues, like
e.g. an exlude "foo" also accidentally excluding "foobar".
"""
sep = os.path.sep
if not root.endswith(sep):
root += sep
for exclude in excludes:
if root.startswith(exclude):
return True
return False
def main():
"""
Parse and check the command line arguments.
"""
parser = optparse.OptionParser(usage="""usage: %prog [options] <package path> [exclude paths, ...]
Note: By default this script will not overwrite already created files.""")
parser.add_option("-n", "--doc-header", action="store", dest="header", help="Documentation Header (default=Project)", default="Project")
parser.add_option("-d", "--dest-dir", action="store", dest="destdir", help="Output destination directory", default="")
parser.add_option("-s", "--suffix", action="store", dest="suffix", help="module suffix (default=txt)", default="txt")
parser.add_option("-m", "--maxdepth", action="store", dest="maxdepth", help="Maximum depth of submodules to show in the TOC (default=4)", type="int", default=4)
parser.add_option("-r", "--dry-run", action="store_true", dest="dryrun", help="Run the script without creating the files")
parser.add_option("-f", "--force", action="store_true", dest="force", help="Overwrite all the files")
parser.add_option("-t", "--no-toc", action="store_true", dest="notoc", help="Don't create the table of content file")
(opts, args) = parser.parse_args()
if not args:
parser.error("package path is required.")
else:
rootpath, excludes = args[0], args[1:]
if os.path.isdir(rootpath):
# check if the output destination is a valid directory
if opts.destdir and os.path.isdir(opts.destdir):
excludes = normalize_excludes(rootpath, excludes)
recurse_tree(rootpath, excludes, opts)
else:
print '%s is not a valid output destination directory.' % opts.destdir
else:
print '%s is not a valid directory.' % rootpath
if __name__ == '__main__':
main()

View File

@ -37,7 +37,7 @@ Create a New Certificate
.. figure:: create_certificate.png
Enter a owner, short description and the authority you wish to issue this certificate.
Enter an owner, short description and the authority you wish to issue this certificate.
Enter a common name into the certificate, if no validity range is selected two years is
the default.

View File

@ -21,7 +21,7 @@ Credential Management
Lemur often contains credentials such as mutual TLS keys or API tokens that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability
to automatically encrypt these keys such that your keys not be in clear text.
The keys are located within lemur/keys and broken down by environment
The keys are located within lemur/keys and broken down by environment.
To utilize this ability use the following commands:
@ -72,7 +72,7 @@ Nginx is a very popular choice to serve a Python project:
Nginx doesn't run any Python process, it only serves requests from outside to
the Python server.
Therefor there are two steps:
Therefore there are two steps:
- Run the Python process.
- Run Nginx.
@ -90,7 +90,7 @@ You must create a Nginx configuration file for Lemur. On GNU/Linux, they usually
go into /etc/nginx/conf.d/. Name it lemur.conf.
`proxy_pass` just passes the external request to the Python process.
The port much match the one used by the 0bin process of course.
The port must match the one used by the Lemur process of course.
You can make some adjustments to get a better user experience::
@ -271,7 +271,7 @@ Create a configuration file named supervisor.ini::
The 4 first entries are just boiler plate to get you started, you can copy
them verbatim.
The last one define one (you can have many) process supervisor should manage.
The last one defines one (you can have many) process supervisor should manage.
It means it will run the command::
@ -293,6 +293,6 @@ Then you can manage the process by running::
supervisorctl -c /path/to/supervisor.ini
It will start a shell from were you can start/stop/restart the service
It will start a shell from which you can start/stop/restart the service.
You can read all errors that might occurs from /tmp/lemur.log.
You can read all errors that might occur from /tmp/lemur.log.

View File

@ -101,7 +101,7 @@ Update your configuration
-------------------------
Once created you will need to update the configuration file with information about your environment,
such as which database to talk to, where keys are stores etc..
such as which database to talk to, where keys are stored etc..
.. Note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so:
postgresql://userame:password@databasefqdn:databaseport/databasename
@ -121,7 +121,7 @@ First, set a password for the postgres user. For this guide, we will use **lemu
Type CTRL-D to exit psql once you have changed the password.
Next, we will create our a new database::
Next, we will create our new database::
$ sudo -u postgres createdb lemur
@ -135,8 +135,8 @@ used by Lemur to help associate certificates that do not currently have an owner
Lemur has discovered certificates from a third party source. This is also a default user that can be used to
administer Lemur.
In addition to create a new User, Lemur also creates a few default email notifications. These notifications are based
on a few configuration options such as `LEMUR_SECURITY_TEAM_EMAIL` they basically garentee that every cerificate within
In addition to creating a new user, Lemur also creates a few default email notifications. These notifications are based
on a few configuration options such as `LEMUR_SECURITY_TEAM_EMAIL`. They basically guarantee that every cerificate within
Lemur will send one expiration notification to the security team.
Additional notifications can be created through the UI or API.
@ -238,7 +238,7 @@ See :ref:`Using Supervisor <UsingSupervisor>` for more details on using Supervis
Syncing
-------
Lemur uses periodic sync tasks to make sure it is up-to-date with it's environment. As always things can change outside
Lemur uses periodic sync tasks to make sure it is up-to-date with its environment. As always things can change outside
of Lemur, but we do our best to reconcile those changes.
.. code-block:: bash
@ -254,7 +254,7 @@ If you're familiar with Python you'll quickly find yourself at home, and even mo
``lemur`` command is just a simple wrapper around Flask's ``manage.py``, which means you get all of the
power and flexibility that goes with it.
Some of those which you'll likely find useful are:
Some of the features which you'll likely find useful are:
lock
~~~~
@ -273,6 +273,6 @@ What's Next?
Get familiar with how Lemur works by reviewing the :doc:`../guide/index`. When you're ready
see :doc:`../production/index` for more details on how to configure Lemur for production.
Remember the above just gets you going, but for production there are several different security considerations to take into account,
remember Lemur is handling sensitive data and security is imperative.
The above just gets you going, but for production there are several different security considerations to take into account.
Remember, Lemur is handling sensitive data and security is imperative.

66
docs/security.rst Normal file
View File

@ -0,0 +1,66 @@
Security
========
We take the security of ``lemur`` seriously. The following are a set of
policies we have adopted to ensure that security issues are addressed in a
timely fashion.
Reporting a security issue
--------------------------
We ask that you do not report security issues to our normal GitHub issue
tracker.
If you believe you've identified a security issue with ``lemur``, please
report it to ``cloudsecurity@netflix.com``.
Once you've submitted an issue via email, you should receive an acknowledgment
within 48 hours, and depending on the action to be taken, you may receive
further follow-up emails.
Supported Versions
------------------
At any given time, we will provide security support for the `master`_ branch
as well as the 2 most recent releases.
Disclosure Process
------------------
Our process for taking a security issue from private discussion to public
disclosure involves multiple steps.
Approximately one week before full public disclosure, we will send advance
notification of the issue to a list of people and organizations, primarily
composed of operating-system vendors and other distributors of
``lemur``. This notification will consist of an email message
containing:
* A full description of the issue and the affected versions of
``lemur``.
* The steps we will be taking to remedy the issue.
* The patches, if any, that will be applied to ``lemur``.
* The date on which the ``lemur`` team will apply these patches, issue
new releases, and publicly disclose the issue.
Simultaneously, the reporter of the issue will receive notification of the date
on which we plan to take the issue public.
On the day of disclosure, we will take the following steps:
* Apply the relevant patches to the ``lemur`` repository. The commit
messages for these patches will indicate that they are for security issues,
but will not describe the issue in any detail; instead, they will warn of
upcoming disclosure.
* Issue the relevant releases.
If a reported issue is believed to be particularly time-sensitive due to a
known exploit in the wild, for example the time between advance notification
and public disclosure may be shortened considerably.
The list of people and organizations who receives advanced notification of
security issues is not and will not be made public. This list generally
consists of high profile downstream distributors and is entirely at the
discretion of the ``lemur`` team.
.. _`master`: https://github.com/Netflix/lemur

View File

@ -72,7 +72,6 @@ gulp.task('dev:styles', function () {
};
var fileList = [
'lemur/static/app/styles/lemur.css',
'bower_components/bootswatch/sandstone/bootswatch.less',
'bower_components/fontawesome/css/font-awesome.css',
'bower_components/angular-spinkit/src/angular-spinkit.css',
@ -81,7 +80,8 @@ gulp.task('dev:styles', function () {
'bower_components/angular-ui-switch/angular-ui-switch.css',
'bower_components/angular-wizard/dist/angular-wizard.css',
'bower_components/ng-table/ng-table.css',
'bower_components/angularjs-toaster/toaster.css'
'bower_components/angularjs-toaster/toaster.css',
'lemur/static/app/styles/lemur.css'
];
return gulp.src(fileList)

View File

@ -35,7 +35,7 @@ class Login(Resource):
Authorization:Bearer <token>
Tokens have a set expiration date. You can inspect the token expiration be base64 decoding the token and inspecting
Tokens have a set expiration date. You can inspect the token expiration by base64 decoding the token and inspecting
it's contents.
.. note:: It is recommended that the token expiration is fairly short lived (hours not days). This will largely depend \

View File

@ -14,7 +14,7 @@ from sqlalchemy import Column, Integer, String, Text, func, ForeignKey, DateTime
from sqlalchemy.dialects.postgresql import JSON
from lemur.database import db
from lemur.certificates.models import cert_get_cn, cert_get_not_after, cert_get_not_before
from lemur.certificates.models import get_cn, get_not_after, get_not_before
class Authority(db.Model):
@ -44,9 +44,9 @@ class Authority(db.Model):
self.owner = owner
self.plugin_name = plugin_name
cert = x509.load_pem_x509_certificate(str(body), default_backend())
self.cn = cert_get_cn(cert)
self.not_before = cert_get_not_before(cert)
self.not_after = cert_get_not_after(cert)
self.cn = get_cn(cert)
self.not_before = get_not_before(cert)
self.not_after = get_not_after(cert)
self.roles = roles
self.description = description

View File

@ -13,9 +13,7 @@ from cryptography.hazmat.backends import default_backend
from sqlalchemy.orm import relationship
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy_utils import EncryptedType
from lemur.utils import get_key
from lemur.utils import Vault
from lemur.database import db
from lemur.plugins.base import plugins
@ -63,7 +61,11 @@ def create_name(issuer, not_before, not_after, subject, san):
return temp.replace(" ", "-")
def cert_get_cn(cert):
def get_signing_algorithm(cert):
return cert.signature_hash_algorithm.name
def get_cn(cert):
"""
Attempts to get a sane common name from a given certificate.
@ -75,7 +77,7 @@ def cert_get_cn(cert):
)[0].value.strip()
def cert_get_domains(cert):
def get_domains(cert):
"""
Attempts to get an domains listed in a certificate.
If 'subjectAltName' extension is not available we simply
@ -96,7 +98,7 @@ def cert_get_domains(cert):
return domains
def cert_get_serial(cert):
def get_serial(cert):
"""
Fetch the serial number from the certificate.
@ -106,7 +108,7 @@ def cert_get_serial(cert):
return cert.serial
def cert_is_san(cert):
def is_san(cert):
"""
Determines if a given certificate is a SAN certificate.
SAN certificates are simply certificates that cover multiple domains.
@ -114,18 +116,18 @@ def cert_is_san(cert):
:param cert:
:return: Bool
"""
if len(cert_get_domains(cert)) > 1:
if len(get_domains(cert)) > 1:
return True
def cert_is_wildcard(cert):
def is_wildcard(cert):
"""
Determines if certificate is a wildcard certificate.
:param cert:
:return: Bool
"""
domains = cert_get_domains(cert)
domains = get_domains(cert)
if len(domains) == 1 and domains[0][0:1] == "*":
return True
@ -133,7 +135,7 @@ def cert_is_wildcard(cert):
return True
def cert_get_bitstrength(cert):
def get_bitstrength(cert):
"""
Calculates a certificates public key bit length.
@ -143,7 +145,7 @@ def cert_get_bitstrength(cert):
return cert.public_key().key_size
def cert_get_issuer(cert):
def get_issuer(cert):
"""
Gets a sane issuer from a given certificate.
@ -160,7 +162,7 @@ def cert_get_issuer(cert):
current_app.logger.error("Unable to get issuer! {0}".format(e))
def cert_get_not_before(cert):
def get_not_before(cert):
"""
Gets the naive datetime of the certificates 'not_before' field.
This field denotes the first date in time which the given certificate
@ -172,7 +174,7 @@ def cert_get_not_before(cert):
return cert.not_valid_before
def cert_get_not_after(cert):
def get_not_after(cert):
"""
Gets the naive datetime of the certificates 'not_after' field.
This field denotes the last date in time which the given certificate
@ -209,7 +211,7 @@ class Certificate(db.Model):
id = Column(Integer, primary_key=True)
owner = Column(String(128))
body = Column(Text())
private_key = Column(EncryptedType(String, get_key))
private_key = Column(Vault)
status = Column(String(128))
deleted = Column(Boolean, index=True)
name = Column(String(128))
@ -224,6 +226,7 @@ class Certificate(db.Model):
not_before = Column(DateTime)
not_after = Column(DateTime)
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
signing_algorithm = Column(String(128))
user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id'))
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate')
@ -237,16 +240,17 @@ class Certificate(db.Model):
self.private_key = private_key
self.chain = chain
cert = x509.load_pem_x509_certificate(str(self.body), default_backend())
self.bits = cert_get_bitstrength(cert)
self.issuer = cert_get_issuer(cert)
self.serial = cert_get_serial(cert)
self.cn = cert_get_cn(cert)
self.san = cert_is_san(cert)
self.not_before = cert_get_not_before(cert)
self.not_after = cert_get_not_after(cert)
self.signing_algorithm = get_signing_algorithm(cert)
self.bits = get_bitstrength(cert)
self.issuer = get_issuer(cert)
self.serial = get_serial(cert)
self.cn = get_cn(cert)
self.san = is_san(cert)
self.not_before = get_not_before(cert)
self.not_after = get_not_after(cert)
self.name = create_name(self.issuer, self.not_before, self.not_after, self.cn, self.san)
for domain in cert_get_domains(cert):
for domain in get_domains(cert):
self.domains.append(Domain(name=domain))
@property

View File

@ -46,6 +46,7 @@ FIELDS = {
'notBefore': fields.DateTime(dt_format='iso8601', attribute='not_before'),
'notAfter': fields.DateTime(dt_format='iso8601', attribute='not_after'),
'cn': fields.String,
'signingAlgorithm': fields.String(attribute='signing_algorithm'),
'status': fields.String,
'body': fields.String
}
@ -319,7 +320,7 @@ class CertificatesList(AuthenticatedResource):
self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate
self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate
self.reqparse.add_argument('authority', type=valid_authority, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('country', type=str, location='json', required=True)
self.reqparse.add_argument('state', type=str, location='json', required=True)
self.reqparse.add_argument('location', type=str, location='json', required=True)
@ -400,6 +401,7 @@ class CertificatesUpload(AuthenticatedResource):
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"signingAlgorithm": "sha2"
"cn": "example.com",
"status": "unknown"
}
@ -543,6 +545,7 @@ class Certificates(AuthenticatedResource):
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"signingAlgorithm": "sha2",
"cn": "example.com",
"status": "unknown"
}
@ -677,6 +680,7 @@ class NotificationCertificatesList(AuthenticatedResource):
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"signingAlgorithm": "sha2",
"cn": "example.com",
"status": "unknown"
}

View File

@ -22,7 +22,8 @@ class InstanceManager(object):
def add(self, class_path):
self.cache = None
self.class_list.append(class_path)
if class_path not in self.class_list:
self.class_list.append(class_path)
def remove(self, class_path):
self.cache = None

View File

@ -8,6 +8,7 @@
from sqlalchemy import func
from lemur import database
from lemur.models import certificate_destination_associations
from lemur.destinations.models import Destination
from lemur.certificates.models import Certificate
@ -117,10 +118,9 @@ def stats(**kwargs):
:param kwargs:
:return:
"""
attr = getattr(Destination, kwargs.get('metric'))
query = database.db.session.query(attr, func.count(attr))
items = query.group_by(attr).all()
items = database.db.session.query(Destination.label, func.count(certificate_destination_associations.c.certificate_id))\
.join(certificate_destination_associations)\
.group_by(Destination.label).all()
keys = []
values = []

View File

@ -72,7 +72,7 @@ SECRET_KEY = '{flask_secret_key}'
# You should consider storing these separately from your config
LEMUR_TOKEN_SECRET = '{secret_token}'
LEMUR_ENCRYPTION_KEY = '{encryption_key}'
LEMUR_ENCRYPTION_KEYS = '{encryption_key}'
# this is a list of domains as regexes that only admins can issue
LEMUR_RESTRICTED_DOMAINS = []
@ -171,7 +171,9 @@ def generate_settings():
settings file.
"""
output = CONFIG_TEMPLATE.format(
encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)),
# we use Fernet.generate_key to make sure that the key length is
# compatible with Fernet
encryption_key=Fernet.generate_key(),
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
)
@ -314,7 +316,7 @@ class CreateUser(Command):
Option('-u', '--username', dest='username', required=True),
Option('-e', '--email', dest='email', required=True),
Option('-a', '--active', dest='active', default=True),
Option('-r', '--roles', dest='roles', default=[])
Option('-r', '--roles', dest='roles', action='append', default=[])
)
def run(self, username, email, active, roles):
@ -716,6 +718,24 @@ def publish_verisign_units():
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
@manager.command
def backfill_signing_algo():
"""
Will attempt to backfill the signing_algorithm column
:return:
"""
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from lemur.certificates.models import get_signing_algorithm
for c in cert_service.get_all_certs():
cert = x509.load_pem_x509_certificate(str(c.body), default_backend())
c.signing_algorithm = get_signing_algorithm(cert)
c.signing_algorithm
database.update(c)
print(c.signing_algorithm)
def main():
manager.add_command("start", LemurServer())
manager.add_command("runserver", Server(host='127.0.0.1'))

View File

@ -0,0 +1,26 @@
"""Adding certificate signing algorithm
Revision ID: 4bcfa2c36623
Revises: 1ff763f5b80b
Create Date: 2015-10-06 10:03:47.993204
"""
# revision identifiers, used by Alembic.
revision = '4bcfa2c36623'
down_revision = '1ff763f5b80b'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('certificates', sa.Column('signing_algorithm', sa.String(length=128), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('certificates', 'signing_algorithm')
### end Alembic commands ###

View File

@ -0,0 +1,255 @@
"""Migrates the private key encrypted column from AES to fernet encryption scheme.
Revision ID: ed422fc58ba
Revises: 4bcfa2c36623
Create Date: 2015-10-23 09:19:28.654126
"""
import base64
# revision identifiers, used by Alembic.
revision = 'ed422fc58ba'
down_revision = '4bcfa2c36623'
import six
from StringIO import StringIO
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.fernet import Fernet, MultiFernet
from flask import current_app
from lemur.common.utils import get_psuedo_random_string
conn = op.get_bind()
#op.drop_table('encrypted_keys')
#op.drop_table('encrypted_passwords')
# helper tables to migrate data
temp_key_table = op.create_table('encrypted_keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('aes', sa.Binary()),
sa.Column('fernet', sa.Binary()),
sa.PrimaryKeyConstraint('id')
)
# helper table to migrate data
temp_password_table = op.create_table('encrypted_passwords',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('aes', sa.Binary()),
sa.Column('fernet', sa.Binary()),
sa.PrimaryKeyConstraint('id')
)
# From http://sqlalchemy-utils.readthedocs.org/en/latest/_modules/sqlalchemy_utils/types/encrypted.html#EncryptedType
# for migration purposes only
class EncryptionDecryptionBaseEngine(object):
"""A base encryption and decryption engine.
This class must be sub-classed in order to create
new engines.
"""
def _update_key(self, key):
if isinstance(key, six.string_types):
key = key.encode()
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(key)
engine_key = digest.finalize()
self._initialize_engine(engine_key)
def encrypt(self, value):
raise NotImplementedError('Subclasses must implement this!')
def decrypt(self, value):
raise NotImplementedError('Subclasses must implement this!')
class AesEngine(EncryptionDecryptionBaseEngine):
"""Provide AES encryption and decryption methods."""
BLOCK_SIZE = 16
PADDING = six.b('*')
def _initialize_engine(self, parent_class_key):
self.secret_key = parent_class_key
self.iv = self.secret_key[:16]
self.cipher = Cipher(
algorithms.AES(self.secret_key),
modes.CBC(self.iv),
backend=default_backend()
)
def _pad(self, value):
"""Pad the message to be encrypted, if needed."""
BS = self.BLOCK_SIZE
P = self.PADDING
padded = (value + (BS - len(value) % BS) * P)
return padded
def encrypt(self, value):
if not isinstance(value, six.string_types):
value = repr(value)
if isinstance(value, six.text_type):
value = str(value)
value = value.encode()
value = self._pad(value)
encryptor = self.cipher.encryptor()
encrypted = encryptor.update(value) + encryptor.finalize()
encrypted = base64.b64encode(encrypted)
return encrypted
def decrypt(self, value):
if isinstance(value, six.text_type):
value = str(value)
decryptor = self.cipher.decryptor()
decrypted = base64.b64decode(value)
decrypted = decryptor.update(decrypted) + decryptor.finalize()
decrypted = decrypted.rstrip(self.PADDING)
if not isinstance(decrypted, six.string_types):
decrypted = decrypted.decode('utf-8')
return decrypted
def migrate_to_fernet(aes_encrypted, old_key, new_key):
"""
Will attempt to migrate an aes encrypted to fernet encryption
:param aes_encrypted:
:return: fernet encrypted value
"""
engine = AesEngine()
engine._update_key(old_key)
if not isinstance(aes_encrypted, six.string_types):
return
aes_decrypted = engine.decrypt(aes_encrypted)
fernet_encrypted = MultiFernet([Fernet(k) for k in new_key]).encrypt(bytes(aes_decrypted))
# sanity check
fernet_decrypted = MultiFernet([Fernet(k) for k in new_key]).decrypt(fernet_encrypted)
if fernet_decrypted != aes_decrypted:
raise Exception("WARNING: Decrypted values do not match!")
return fernet_encrypted
def migrate_from_fernet(fernet_encrypted, old_key, new_key):
"""
Will attempt to migrate from a fernet encryption to aes
:param fernet_encrypted:
:return:
"""
engine = AesEngine()
engine._update_key(new_key)
fernet_decrypted = MultiFernet([Fernet(k) for k in old_key]).decrypt(fernet_encrypted)
aes_encrypted = engine.encrypt(fernet_decrypted)
# sanity check
aes_decrypted = engine.decrypt(aes_encrypted)
if fernet_decrypted != aes_decrypted:
raise Exception("WARNING: Decrypted values do not match!")
return aes_encrypted
def upgrade():
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
print "Using: {0} as decryption key".format(old_key)
# generate a new fernet token
if current_app.config.get('LEMUR_ENCRYPTION_KEYS'):
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
else:
new_key = [Fernet.generate_key()]
print "Using: {0} as new encryption key, save this and place it in your configuration!".format(new_key)
# migrate private_keys
temp_keys = []
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
aes_encrypted = StringIO(private_key).read()
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_key_table, temp_keys)
for id, fernet in conn.execute(text('select id, fernet from encrypted_keys')):
stmt = text("update certificates set private_key=:key where id=:id")
stmt = stmt.bindparams(key=fernet, id=id)
op.execute(stmt)
print "Certificate {0} has been migrated".format(id)
# migrate role_passwords
temp_passwords = []
for id, password in conn.execute(text('select id, password from roles where password is not null')):
aes_encrypted = StringIO(password).read()
fernet_encrypted = migrate_to_fernet(aes_encrypted, old_key, new_key)
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_password_table, temp_passwords)
for id, fernet in conn.execute(text('select id, fernet from encrypted_passwords')):
stmt = text("update roles set password=:password where id=:id")
stmt = stmt.bindparams(password=fernet, id=id)
print stmt
op.execute(stmt)
print "Password {0} has been migrated".format(id)
op.drop_table('encrypted_keys')
op.drop_table('encrypted_passwords')
def downgrade():
old_key = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
print "Using: {0} as decryption key(s)".format(old_key)
# generate aes valid key
if current_app.config.get('LEMUR_ENCRYPTION_KEY'):
new_key = current_app.config.get('LEMUR_ENCRYPTION_KEY')
else:
new_key = get_psuedo_random_string()
print "Using: {0} as the encryption key, save this and place it in your configuration!".format(new_key)
# migrate keys
temp_keys = []
for id, private_key in conn.execute(text('select id, private_key from certificates where private_key is not null')):
fernet_encrypted = StringIO(private_key).read()
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
temp_keys.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_key_table, temp_keys)
for id, aes in conn.execute(text('select id, aes from encrypted_keys')):
stmt = text("update certificates set private_key=:key where id=:id")
stmt = stmt.bindparams(key=aes, id=id)
print stmt
op.execute(stmt)
print "Certificate {0} has been migrated".format(id)
# migrate role_passwords
temp_passwords = []
for id, password in conn.execute(text('select id, password from roles where password is not null')):
fernet_encrypted = StringIO(password).read()
aes_encrypted = migrate_from_fernet(fernet_encrypted, old_key, new_key)
temp_passwords.append({'id': id, 'aes': aes_encrypted, 'fernet': fernet_encrypted})
op.bulk_insert(temp_password_table, temp_passwords)
for id, aes in conn.execute(text('select id, aes from encrypted_passwords')):
stmt = text("update roles set password=:password where id=:id")
stmt = stmt.bindparams(password=aes, id=id)
op.execute(stmt)
print "Password {0} has been migrated".format(id)
op.drop_table('encrypted_keys')
op.drop_table('encrypted_passwords')

View File

@ -86,7 +86,7 @@ class PluginsList(AuthenticatedResource):
if args['type']:
return list(plugins.all(plugin_type=args['type']))
return plugins.all()
return list(plugins.all())
class Plugins(AuthenticatedResource):

View File

@ -12,9 +12,8 @@
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, Text, ForeignKey
from sqlalchemy_utils import EncryptedType
from lemur.database import db
from lemur.utils import get_key
from lemur.utils import Vault
from lemur.models import roles_users
@ -23,7 +22,7 @@ class Role(db.Model):
id = Column(Integer, primary_key=True)
name = Column(String(128), unique=True)
username = Column(String(128))
password = Column(EncryptedType(String, get_key))
password = Column(Vault)
description = Column(Text)
authority_id = Column(Integer, ForeignKey('authorities.id'))
user_id = Column(Integer, ForeignKey('users.id'))

View File

@ -2,7 +2,7 @@
var lemur = angular
.module('lemur', [
'ngRoute',
'ui.router',
'ngTable',
'ngAnimate',
'chart.js',
@ -13,15 +13,17 @@ var lemur = angular
'toaster',
'uiSwitch',
'mgo-angular-wizard',
'satellizer'
'satellizer',
'ngLetterAvatar',
'angular-clipboard'
])
.config(function ($routeProvider, $authProvider) {
$routeProvider
.when('/', {
.config(function ($stateProvider, $urlRouterProvider, $authProvider) {
$urlRouterProvider.otherwise('/welcome');
$stateProvider
.state('welcome', {
url: '/welcome',
templateUrl: 'angular/welcome/welcome.html'
})
.otherwise({
redirectTo: '/'
});
$authProvider.oauth2({

View File

@ -1,8 +1,9 @@
'use strict';
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/login', {
.config(function config($stateProvider) {
$stateProvider.state('login', {
url: '/login',
templateUrl: '/angular/authentication/login/login.tpl.html',
controller: 'LoginController'
});

View File

@ -1,9 +1,10 @@
'use strict';
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/logout', {
controller: 'LogoutCtrl'
.config(function config($stateProvider) {
$stateProvider.state('logout', {
controller: 'LogoutCtrl',
url: '/logout'
});
})
.controller('LogoutCtrl', function ($scope, $location, lemurRestangular, userService) {

View File

@ -2,15 +2,22 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/authorities', {
templateUrl: '/angular/authorities/view/view.tpl.html',
controller: 'AuthoritiesViewController'
});
.config(function config($stateProvider) {
$stateProvider
.state('authorities', {
url: '/authorities',
templateUrl: '/angular/authorities/view/view.tpl.html',
controller: 'AuthoritiesViewController'
})
.state('authority', {
url: '/authorities/:name',
templateUrl: '/angular/authorities/view/view.tpl.html',
controller: 'AuthoritiesViewController'
});
})
.controller('AuthoritiesViewController', function ($scope, $q, $modal, AuthorityApi, AuthorityService, ngTableParams) {
$scope.filter = {};
.controller('AuthoritiesViewController', function ($scope, $q, $modal, $stateParams, AuthorityApi, AuthorityService, ngTableParams) {
$scope.filter = $stateParams;
$scope.authoritiesTable = new ngTableParams({
page: 1, // show first page
count: 10, // count per page

View File

@ -34,6 +34,9 @@
</a>
</div>
</td>
<td data-title="''">
<a ui-sref="authority({'name': '{{ authority.name }}'})">Permalink</a>
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<button tooltip="Edit Authority" ng-click="edit(authority.id)" class="btn btn-sm btn-info">

View File

@ -2,15 +2,23 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/certificates', {
templateUrl: '/angular/certificates/view/view.tpl.html',
controller: 'CertificatesViewController'
});
.config(function config($stateProvider) {
$stateProvider
.state('certificates', {
url: '/certificates',
templateUrl: '/angular/certificates/view/view.tpl.html',
controller: 'CertificatesViewController'
})
.state('certificate', {
url: '/certificates/:name',
templateUrl: '/angular/certificates/view/view.tpl.html',
controller: 'CertificatesViewController'
});
})
.controller('CertificatesViewController', function ($q, $scope, $modal, CertificateApi, CertificateService, MomentService, ngTableParams) {
$scope.filter = {};
.controller('CertificatesViewController', function ($q, $scope, $modal, $stateParams, CertificateApi, CertificateService, MomentService, ngTableParams) {
$scope.filter = $stateParams;
$scope.certificateTable = new ngTableParams({
page: 1, // show first page
count: 10, // count per page

View File

@ -6,11 +6,11 @@
<div class="panel-heading">
<div class="btn-group pull-right">
<button data-placement="left" data-title="Create Certificate" bs-tooltip ng-click="create()"
class="btn btn-primary">
class="btn btn-primary">
Create
</button>
<button data-placement="left" data-title="Import Certificate" bs-tooltip ng-click="import()"
class="btn btn-info">
class="btn btn-info">
Import
</button>
</div>
@ -32,7 +32,8 @@
</td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
<form>
<switch ng-change="certificateService.updateActive(certificate)" id="status" name="status" ng-model="certificate.active" class="green small"></switch>
<switch ng-change="certificateService.updateActive(certificate)" id="status" name="status"
ng-model="certificate.active" class="green small"></switch>
</form>
</td>
<td data-title="'Issuer'" sortable="'issuer'" filter="{ 'issuer': 'text' }">
@ -41,105 +42,130 @@
<td data-title="'Common Name'" filter="{ 'cn': 'text'}">
{{ certificate.cn }}
</td>
<td data-title="''">
<a ui-sref="certificate({'name': '{{ certificate.name }}'})">Permalink</a>
</td>
<td data-title="''">
<div class="btn-group pull-right">
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1" butn-checkbox-false="0">More</button>
<button class="btn btn-sm btn-warning" ng-click="edit(certificate.id)">Edit</button>
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1"
butn-checkbox-false="0">More
</button>
<button class="btn btn-sm btn-warning" ng-click="edit(certificate.id)">Edit</button>
</div>
</td>
</tr>
<tr class="warning" ng-show="certificate.toggle" ng-repeat-end>
<td colspan="6">
<tabset justified="true" class="col-md-6">
<tab heading="Basic Info">
<ul class="list-group">
<li class="list-group-item">
<strong>Creator</strong>
<span class="pull-right">
{{ certificate.creator.email }}
</span>
</li>
<li class="list-group-item">
<strong>Not Before</strong>
<span class="pull-right" tooltip="{{ certificate.notBefore }}">
{{ momentService.createMoment(certificate.notBefore) }}
</span>
</li>
<li class="list-group-item">
<strong>Not After</strong>
<span class="pull-right" tooltip="{{ certificate.notAfter }}">
{{ momentService.createMoment(certificate.notAfter) }}
</span>
</li>
<li class="list-group-item">
<strong>San</strong>
<span class="pull-right">
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
</span>
</li>
<li class="list-group-item">
<strong>Bits</strong>
<span class="pull-right">{{ certificate.bits }}</span>
</li>
<li class="list-group-item">
<strong>Serial</strong>
<span class="pull-right">{{ certificate.serial }}</span>
</li>
<li tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked" class="list-group-item">
<strong>Validity</strong>
<span class="pull-right">
<span ng-show="!certificate.status" class="label label-warning">Unknown</span>
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
</span>
</li>
<li class="list-group-item">
<strong>Description</strong>
<span class="pull-right">{{ certificate.description }}</span>
</li>
</ul>
</tab>
<tab heading="Notifications">
<ul class="list-group">
<li class="list-group-item" ng-repeat="notification in certificate.notifications">
<strong>{{ notification.label }}</strong>
<span class="pull-right">{{ notification.description}}</span>
</li>
</ul>
</tab>
<tab heading="Destinations">
<ul class="list-group">
<li class="list-group-item" ng-repeat="destination in certificate.destinations">
<strong>{{ destination.label }}</strong>
<span class="pull-right">{{ destination.description }}</span>
</li>
</ul>
</tab>
<tab heading="Domains">
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
</div>
</tab>
<tabset justified="true" class="col-md-6">
<tab>
<tab-heading>Basic Info</tab-heading>
<ul class="list-group">
<li class="list-group-item">
<strong>Creator</strong>
<span class="pull-right">
{{ certificate.creator.email }}
</span>
</li>
<li class="list-group-item">
<strong>Not Before</strong>
<span class="pull-right" tooltip="{{ certificate.notBefore }}">
{{ momentService.createMoment(certificate.notBefore) }}
</span>
</li>
<li class="list-group-item">
<strong>Not After</strong>
<span class="pull-right" tooltip="{{ certificate.notAfter }}">
{{ momentService.createMoment(certificate.notAfter) }}
</span>
</li>
<li class="list-group-item">
<strong>San</strong>
<span class="pull-right">
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
</span>
</li>
<li class="list-group-item">
<strong>Bits</strong>
<span class="pull-right">{{ certificate.bits }}</span>
</li>
<li class="list-group-item">
<strong>Signing Algorithm</strong>
<span class="pull-right">{{ certificate.signingAlgorithm }}</span>
</li>
<li class="list-group-item">
<strong>Serial</strong>
<span class="pull-right">{{ certificate.serial }}</span>
</li>
<li
tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked"
class="list-group-item">
<strong>Validity</strong>
<span class="pull-right">
<span ng-show="!certificate.status" class="label label-warning">Unknown</span>
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
</span>
</li>
<li class="list-group-item">
<strong>Description</strong>
<span class="pull-right">{{ certificate.description }}</span>
</li>
</ul>
</tab>
<tab>
<tab-heading>Notifications</tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="notification in certificate.notifications">
<strong>{{ notification.label }}</strong>
<span class="pull-right">{{ notification.description}}</span>
</li>
</ul>
</tab>
<tab>
<tab-heading>Destinations</tab-heading>
<ul class="list-group">
<li class="list-group-item" ng-repeat="destination in certificate.destinations">
<strong>{{ destination.label }}</strong>
<span class="pull-right">{{ destination.description }}</span>
</li>
</ul>
</tab>
<tab>
<tab-heading>Domains</tab-heading>
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item"
ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
</div>
</tab>
</tabset>
<tabset justified="true" class="col-md-6">
<tab heading="Chain">
<p>
<pre style="width: 550px">{{ certificate.chain }}</pre>
</p>
<tab>
<tab-heading>
Chain
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy chain to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.chain"></button>
</tab-heading>
<pre style="width: 100%">{{ certificate.chain }}</pre>
</tab>
<tab heading="Public Certificate">
<p>
<pre style="width: 550px">{{ certificate.body }}</pre>
</p>
<tab>
<tab-heading>
Public Certificate
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy certificate to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.body"></button>
</tab-heading>
<pre style="width: 100%">{{ certificate.body }}</pre>
</tab>
<tab ng-click="certificateService.loadPrivateKey(certificate)">
<tab-heading>
Private Key
<button class="btn btn-xs btn-default clipboard-btn glyphicon glyphicon-copy"
tooltip="Copy key to clipboard" tooltip-trigger="mouseenter" clipboard
text="certificate.privateKey"></button>
</tab-heading>
<p>
<pre style="width: 550px">{{ certificate.privateKey }}</pre>
</p>
<pre style="width: 100%">{{ certificate.privateKey }}</pre>
</tab>
</tabset>
</td>

View File

@ -1,8 +1,9 @@
'use strict';
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/dashboard', {
.config(function config($stateProvider) {
$stateProvider.state('dashboard', {
url: '/dashboard',
templateUrl: '/angular/dashboard/dashboard.tpl.html',
controller: 'DashboardController'
});
@ -78,13 +79,18 @@ angular.module('lemur')
$scope.bits = data.items;
});
LemurRestangular.all('certificates').customGET('stats', {metric: 'signing_algorithm'})
.then(function (data) {
$scope.algos = data.items;
});
LemurRestangular.all('certificates').customGET('stats', {metric: 'not_after'})
.then(function (data) {
$scope.expiring = {labels: data.items.labels, values: [data.items.values]};
});
LemurRestangular.all('destinations').customGET('stats', {metric: 'certificates'})
LemurRestangular.all('destinations').customGET('stats', {metric: 'certificate'})
.then(function (data) {
$scope.destinations = {labels: data.items.labels, values: [data.items.values]};
$scope.destinations = data.items;
});
});

View File

@ -10,43 +10,54 @@
<h3 class="panel-title">Expiring Certificates</h3>
</div>
<div class="panel-body">
<canvas id="expiringBar" class="chart chart-bar" data="expiring.values" labels="expiring.labels" colours="colours"></canvas>
<canvas id="expiringBar" class="chart chart-bar" data="expiring.values" labels="expiring.labels"
colours="colours"></canvas>
</div>
</div>
</div>
</div>
<div class="row"></div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Issuers</h3>
</div>
<div class="panel-body">
<canvas id="issuersPie" class="chart chart-pie" data="issuers.values" labels="issuers.labels" colours="colours" legend="true"></canvas>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Issuers</h3>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Bit Strength</h3>
</div>
<div class="panel-body">
<canvas id="bitsPie" class="chart chart-pie" data="bits.values" labels="bits.labels" colours="colours" legend="true"></canvas>
</div>
<div class="panel-body">
<canvas id="issuersPie" class="chart chart-pie" data="issuers.values" labels="issuers.labels" colours="colours"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Bit Strength</h3>
</div>
<div class="panel-body">
<canvas id="bitsPie" class="chart chart-pie" data="bits.values" labels="bits.labels" colours="colours"></canvas>
</div>
</div>
</div>
<div class="row"></div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Destinations</h3>
</div>
<div class="panel-body">
<canvas id="destinationPie" class="chart chart-pie" data="destinations.values" labels="destinations.labels"
colours="colours"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Signing Algorithms</h3>
</div>
<div class="panel-body">
<canvas id="signingPie" class="chart chart-pie" data="algos.values" labels="algos.labels"
colours="colours"></canvas>
</div>
</div>
<div class="row"></div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Destinations</h3>
</div>
<div class="panel-body">
<canvas id="destinationPie" class="chart chart-pie" data="destinations.values" labels="destinations.labels" colours="colours" legend="true"></canvas>
</div>
</div>
</div>
</div>
<!-- /.row -->
</div>

View File

@ -2,8 +2,9 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/destinations', {
.config(function config($stateProvider) {
$stateProvider.state('destinations', {
url: '/destinations',
templateUrl: '/angular/destinations/view/view.tpl.html',
controller: 'DestinationsViewController'
});

View File

@ -2,8 +2,9 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/domains', {
.config(function config($stateProvider) {
$stateProvider.state('domains', {
url: '/domains',
templateUrl: '/angular/domains/view/view.tpl.html',
controller: 'DomainsViewController'
});

View File

@ -2,8 +2,9 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/notifications', {
.config(function config($stateProvider) {
$stateProvider.state('notifications', {
url: '/notifications',
templateUrl: '/angular/notifications/view/view.tpl.html',
controller: 'NotificationsViewController'
});

View File

@ -2,8 +2,9 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/roles', {
.config(function config($stateProvider) {
$stateProvider.state('roles', {
url: '/roles',
templateUrl: '/angular/roles/view/view.tpl.html',
controller: 'RolesViewController'
});

View File

@ -2,8 +2,9 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/sources', {
.config(function config($stateProvider) {
$stateProvider.state('sources', {
url: '/sources',
templateUrl: '/angular/sources/view/view.tpl.html',
controller: 'SourcesViewController'
});

View File

@ -2,8 +2,9 @@
angular.module('lemur')
.config(function config($routeProvider) {
$routeProvider.when('/users', {
.config(function config($stateProvider) {
$stateProvider.state('users', {
url: '/users',
templateUrl: '/angular/users/view/view.tpl.html',
controller: 'UsersViewController'
});

View File

@ -1,12 +1,26 @@
<div class="jumbotron">
<span class="pull-right"><button class="btn btn-sm btn-primary">First Time? Take the Tour!</button></span>
<h1>Hey there!</h1>
<p>Welcome to Lemur! A central portal for all (most) of your TLS needs.</p>
<p><a href="/#/certificates/create" class="btn btn-primary btn-lg" role="button">Create a Certificate</a></p>
<p>Welcome to Lemur! A central portal for all (most) of your TLS certificate needs. With Lemur you are able to create, deploy and track the TLS certificates in your environment. Lets get started!</p>
</div>
<div class="row featurette">
<div class="col-md-10">
<h2 class="featurette-heading">TLS In The Cloud <span class="text-muted">Encrypt it all </span></h2>
</div>
<div class="container marketing">
<!-- Three columns of text below the carousel -->
<div class="row">
<div class="col-lg-4">
<h2>Create</h2>
<p>With Lemur you can create certificates from any authority; internal or external! Lemur does not issue certificates itself. Instead it acts as a broker, creating private keys and CSRs that are sent to external services.</p>
<p><a class="btn btn-default" ui-sref="certificates" role="button">View certificates &raquo;</a></p>
</div><!-- /.col-lg-4 -->
<div class="col-lg-4">
<h2>Deploy</h2>
<p>Once certificates have been created with Lemur, you can put them to use! Lemur has the ability to create destinations for certificates that allow them to be uploaded to and used by a variety of environments.</p>
<p><a class="btn btn-default" ui-sref="destinations" role="button">View Destinations &raquo;</a></p>
</div><!-- /.col-lg-4 -->
<div class="col-lg-4">
<h2>Authority</h2>
<p>Have an internal Certificate Authority? Need an easy way to create an manage those authorities? Lemur has you covered!</p>
<p><a class="btn btn-default" ui-sref="authorities" role="button">View Authorities &raquo;</a></p>
</div><!-- /.col-lg-4 -->
</div><!-- /.row -->
</div>

View File

@ -49,45 +49,53 @@
</div>
<div class="navbar-collapse collapse" ng-controller="LoginController">
<ul class="nav navbar-nav navbar-left">
<li><a href="/#/dashboard">Dashboard</a></li>
<li><a href="/#/certificates">Certificates</a></li>
<li><a href="/#/authorities">Authorities</a></li>
<li><a href="/#/notifications">Notifications</a></li>
<li><a href="/#/destinations">Destinations</a></li>
<li><a href="/#/sources">Sources</a></li>
<li><a ui-sref="dashboard">Dashboard</a></li>
<li><a ui-sref="certificates">Certificates</a></li>
<li><a ui-sref="authorities">Authorities</a></li>
<li><a ui-sref="notifications">Notifications</a></li>
<li><a ui-sref="destinations">Destinations</a></li>
<li><a ui-sref="sources">Sources</a></li>
<li></li>
<li class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle" dropdown-toggle>Settings <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/#/roles">Roles</a></li>
<li><a href="/#/users">Users</a></li>
<li><a href="/#/domains">Domains</a></li>
<li><a ui-sref="roles">Roles</a></li>
<li><a ui-sref="users">Users</a></li>
<li><a ui-sref="domains">Domains</a></li>
</ul>
</li>
</ul>
<ul ng-show="!currentUser.username" class="nav navbar-nav navbar-right">
<li><a href="/#/login">Login</a></li>
<li><a ui-sref="login">Login</a></li>
</ul>
<ul ng-show="currentUser.username" class="nav navbar-nav navbar-right">
<li class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle profile-nav" dropdown-toggle>
{{ currentUser.username }}<img ng-if="currentUser.profileImage" src="{{ currentUser.profileImage }}" class="profile img-circle">
</a>
<ul class="dropdown-menu">
<li><a ng-click="logout()">Logout</a></li>
</ul>
</li>
<li class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle profile-nav" dropdown-toggle>
<span ng-show="currentUser.profileImage">
{{ currentUser.username }}<img src="{{ currentUser.profileImage }}" class="profile img-circle">
</span>
<span ng-show="!currentUser.profileImage">
{{ currentUser.username }}<ng-letter-avatar height="35" width="35" data="currentUser.username" shape="round"></ng-letter-avatar>
</span>
</a>
<ul class="dropdown-menu">
<li><a ng-click="logout()">Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<!-- Add your site or application content here -->
<div class="container-fluid">
<div ng-view></div>
<div ui-view></div>
</div>
<footer class="footer">
<div class="container">
<p class="text-muted">Lemur is broken regularly by <a href="https://github.com/Netflix/lemur.git">Netflix</a>.</p>
<p class="text-muted">
<span>Lemur is broken regularly by <a href="https://github.com/Netflix/lemur.git">Netflix</a>.</span>
<span class="pull-right">Confused? Check out our <a href="https://lemur.readthedocs.org/en/latest">docs</a>!</span>
</p>
</div>
</footer>
</body>

View File

@ -169,3 +169,13 @@ a {
background-color: #FFFFFF !important;
}
.clipboard-btn {
border-width: 0;
background-color: transparent;
color: #777;
display: inline-block;
top: 0;
line-height: 1;
}

View File

@ -21,7 +21,7 @@ SECRET_KEY = 'I/dVhOZNSMZMqrFJa5tWli6VQccOGudKerq3eWPMSzQNmHHVhMAQfQ=='
# You should consider storing these separately from your config
LEMUR_TOKEN_SECRET = 'test'
LEMUR_ENCRYPTION_KEY = 'jPd2xwxgVGXONqghHNq7/S761sffYSrT3UAgKwgtMxbqa0gmKYCfag=='
LEMUR_ENCRYPTION_KEYS = 'o61sBLNBSGtAckngtNrfVNd8xy8Hp9LBGDstTbMbqCY='
# this is a list of domains as regexes that only admins can issue
LEMUR_RESTRICTED_DOMAINS = []

View File

@ -41,44 +41,44 @@ def test_create_basic_csr():
def test_cert_get_cn():
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_cn
from lemur.certificates.models import get_cn
assert cert_get_cn(INTERNAL_VALID_LONG_CERT) == 'long.lived.com'
assert get_cn(INTERNAL_VALID_LONG_CERT) == 'long.lived.com'
def test_cert_get_subAltDomains():
from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_domains
from lemur.certificates.models import get_domains
assert cert_get_domains(INTERNAL_VALID_LONG_CERT) == []
assert cert_get_domains(INTERNAL_VALID_SAN_CERT) == ['example2.long.com', 'example3.long.com']
assert get_domains(INTERNAL_VALID_LONG_CERT) == []
assert get_domains(INTERNAL_VALID_SAN_CERT) == ['example2.long.com', 'example3.long.com']
def test_cert_is_san():
from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_is_san
from lemur.certificates.models import is_san
assert cert_is_san(INTERNAL_VALID_LONG_CERT) == None # noqa
assert cert_is_san(INTERNAL_VALID_SAN_CERT) == True # noqa
assert is_san(INTERNAL_VALID_LONG_CERT) == None # noqa
assert is_san(INTERNAL_VALID_SAN_CERT) == True # noqa
def test_cert_is_wildcard():
from lemur.tests.certs import INTERNAL_VALID_WILDCARD_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_is_wildcard
assert cert_is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True # noqa
assert cert_is_wildcard(INTERNAL_VALID_LONG_CERT) == None # noqa
from lemur.certificates.models import is_wildcard
assert is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True # noqa
assert is_wildcard(INTERNAL_VALID_LONG_CERT) == None # noqa
def test_cert_get_bitstrength():
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_bitstrength
assert cert_get_bitstrength(INTERNAL_VALID_LONG_CERT) == 2048
from lemur.certificates.models import get_bitstrength
assert get_bitstrength(INTERNAL_VALID_LONG_CERT) == 2048
def test_cert_get_issuer():
from lemur.tests.certs import INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_get_issuer
assert cert_get_issuer(INTERNAL_VALID_LONG_CERT) == 'Example'
from lemur.certificates.models import get_issuer
assert get_issuer(INTERNAL_VALID_LONG_CERT) == 'Example'
def test_get_name_from_arn():

View File

@ -5,17 +5,93 @@
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import six
from flask import current_app
from cryptography.fernet import Fernet, MultiFernet
import sqlalchemy.types as types
def get_key():
def get_keys():
"""
Gets the current encryption key
Gets the encryption keys.
This supports multiple keys to facilitate key rotation. The first
key in the list is used to encrypt. Decryption is attempted with
each key in succession.
:return:
"""
# when running lemur create_config, this code needs to work despite
# the fact that there is not a current_app with a config at that point
try:
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip()
except RuntimeError:
print("No Encryption Key Found")
return ''
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
except:
print("no encryption keys")
return []
# this function is expected to return a list of keys, but we want
# to let people just specify a single key
if not isinstance(keys, list):
keys = [keys]
# make sure there is no accidental whitespace
keys = [key.strip() for key in keys]
return keys
class Vault(types.TypeDecorator):
"""
A custom SQLAlchemy column type that transparently handles encryption.
This uses the MultiFernet from the cryptography package to faciliate
key rotation. That class handles encryption and signing.
Fernet uses AES in CBC mode with 128-bit keys and PKCS7 padding. It
uses HMAC-SHA256 for ciphertext authentication. Initialization
vectors are generated using os.urandom().
"""
# required by SQLAlchemy. defines the underlying column type
impl = types.Binary
def process_bind_param(self, value, dialect):
"""
Encrypt values on the way into the database.
MultiFernet.encrypt uses the first key in the list.
"""
# we assume that the user's keys are already Fernet keys (32 byte
# keys that have been base64 encoded).
self.keys = [Fernet(key) for key in get_keys()]
# we only support strings and they should be of type bytes for Fernet
if not isinstance(value, six.string_types):
return None
value = bytes(value)
return MultiFernet(self.keys).encrypt(value)
def process_result_value(self, value, dialect):
"""
Decrypt values on the way out of the database.
MultiFernet tries each key until one works.
"""
# we assume that the user's keys are already Fernet keys (32 byte
# keys that have been base64 encoded).
self.keys = [Fernet(key) for key in get_keys()]
# if the value is not a string we aren't going to try to decrypt
# it. this is for the case where the column is null
if not isinstance(value, six.string_types):
return None
# TODO this may raise an InvalidToken exception in certain
# cases. Should we handle that?
# https://cryptography.io/en/latest/fernet/#cryptography.fernet.Fernet.decrypt
return MultiFernet(self.keys).decrypt(value)

View File

@ -1,6 +1,5 @@
{
"name": "Lemur",
"version": "0.0.0",
"private": true,
"repository": {
"type": "git",

View File

@ -41,7 +41,7 @@ install_requires = [
'six==1.9.0',
'gunicorn==19.3.0',
'pycrypto==2.6.1',
'cryptography==1.0.1',
'cryptography==1.0.2',
'pyopenssl==0.15.1',
'pyjwt==1.0.1',
'xmltodict==0.9.2',
@ -124,7 +124,7 @@ class BuildStatic(Command):
setup(
name='lemur',
version='0.1.4',
version='0.1.5',
author='Kevin Glisson',
author_email='kglisson@netflix.com',
url='https://github.com/netflix/lemur',