Compare commits
113 Commits
Author | SHA1 | Date | |
---|---|---|---|
56f7da34d7 | |||
0f34440b64 | |||
bbcc7cca4e | |||
0453afcb0e | |||
cafecd1e19 | |||
4b968a9474 | |||
9244945e69 | |||
78819c1733 | |||
394e18f76e | |||
40eb950e94 | |||
90636a5329 | |||
2fc6d4cd21 | |||
b20bdf3c4e | |||
a20726a301 | |||
39727a1c9f | |||
168f46a436 | |||
4ec07a6dc7 | |||
798a6295ee | |||
73cb8da8c1 | |||
3167ce9785 | |||
63b7b71b49 | |||
9965af9ccd | |||
ba5d2c925a | |||
867be09e29 | |||
8362a92898 | |||
162482dbc4 | |||
c0f14db5bb | |||
3c561914c6 | |||
34c6f1bf4d | |||
2187898494 | |||
d4bc6ae7a1 | |||
81cdb15353 | |||
5cfa9d4bc5 | |||
92da453233 | |||
2aedfedbd3 | |||
64c9b11c09 | |||
5f87c87751 | |||
70f9022aae | |||
43683fe554 | |||
002de6f5e4 | |||
63a388236e | |||
9560791002 | |||
ed93b5a2c5 | |||
21e4cc9f4d | |||
73e628cbdf | |||
7ebd0bf5d4 | |||
3f1902e0fe | |||
3e546eaa21 | |||
e70deb155d | |||
4f289c790b | |||
c15f525167 | |||
bcbf642122 | |||
1559727f2d | |||
a596793a9a | |||
862bf3f619 | |||
83a86c06a4 | |||
06a69c09a0 | |||
6a24e88d9a | |||
be6a5b859e | |||
2444191bf2 | |||
9226b1eb4a | |||
3f53629175 | |||
baef329a4d | |||
b103fc7bfb | |||
a3385bd2ac | |||
7cb50c654b | |||
52ba538037 | |||
0a0460529f | |||
fc0a884d5f | |||
dbbea29e75 | |||
bcd0aae8c6 | |||
50d3e6aff2 | |||
1d45926122 | |||
45626c947c | |||
d7ca6d4327 | |||
6411bd56e9 | |||
1486e7b8f6 | |||
e73f2bcb2b | |||
a412569ff7 | |||
387194d651 | |||
13d0359041 | |||
365d927efb | |||
aa76379d6c | |||
ef72de89b3 | |||
e2962a4b8d | |||
a563986ce4 | |||
a4e294634a | |||
067122f8f4 | |||
6a1a744eff | |||
d3cf273a45 | |||
52e267468a | |||
e80b58899d | |||
25f652c1eb | |||
0dde4c9f80 | |||
a512b82196 | |||
7f119e95e1 | |||
bf957d2509 | |||
1e314b505f | |||
ef9a80ebfd | |||
84d0afae4c | |||
48a53ad436 | |||
2f4aee49e2 | |||
f3f5b9eeb3 | |||
541d5420bb | |||
0383e2a1e1 | |||
084604cf3c | |||
8ab9c06778 | |||
0afd4c94b4 | |||
aaae4d5a1f | |||
9da713ab06 | |||
540d56f0b8 | |||
160eaa6901 | |||
180c8228e1 |
3
AUTHORS
@ -1,2 +1,3 @@
|
||||
- Kevin Glisson (kglisson@netflix.com)
|
||||
- Kevin Glisson <kglisson@netflix.com>
|
||||
- Jeremy Heffner <jheffner@netflix.com>
|
||||
|
||||
|
15
CHANGELOG.rst
Normal 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>`_
|
4
MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
include setup.py package.json bower.json gulpfile.js README.rst MANIFEST.in LICENSE AUTHORS
|
||||
recursive-include lemur/plugins/lemur_email/templates *
|
||||
recursive-include lemur/static *
|
||||
global-exclude *~
|
2
Makefile
@ -9,6 +9,8 @@ develop: update-submodules setup-git
|
||||
pip install -e .
|
||||
pip install "file://`pwd`#egg=lemur[dev]"
|
||||
pip install "file://`pwd`#egg=lemur[tests]"
|
||||
node_modules/.bin/gulp build
|
||||
node_modules/.bin/gulp package
|
||||
@echo ""
|
||||
|
||||
dev-docs:
|
||||
|
16
README.rst
@ -13,18 +13,24 @@ Lemur
|
||||
:target: https://lemur.readthedocs.org
|
||||
:alt: Latest Docs
|
||||
|
||||
.. image:: https://magnum.travis-ci.com/Netflix/lemur.svg?branch=master
|
||||
:target: https://magnum.travis-ci.com/Netflix/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 SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults.
|
||||
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.
|
||||
|
||||
It works on CPython 2.7, 3.3, 3.4 It is known
|
||||
to work on Ubuntu Linux and OS X.
|
||||
|
||||
Project resources
|
||||
=================
|
||||
|
||||
- `Lemur Blog Post <http://techblog.netflix.com/2015/09/introducing-lemur.html>`_
|
||||
- `Documentation <http://lemur.readthedocs.org/>`_
|
||||
- `Source code <https://github.com/netflix/lemur>`_
|
||||
- `Issue tracker <https://github.com/netflix/lemur/issues>`_
|
||||
- `Docker <https://github.com/Netflix/lemur-docker>`_
|
||||
|
@ -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",
|
||||
|
@ -2,7 +2,7 @@ Configuration
|
||||
=============
|
||||
|
||||
.. warning::
|
||||
There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configruation
|
||||
There are many secrets that Lemur uses that must be protected. All of these options are set via the Lemur configuration
|
||||
file. It is highly advised that you do not store your secrets in this file! Lemur provides functions
|
||||
that allow you to encrypt files at rest and decrypt them when it's time for deployment. See :ref:`Credential Management <CredentialManagement>`
|
||||
for more information.
|
||||
@ -72,9 +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.
|
||||
|
||||
See `SECRET_KEY` for methods on secure secret generation.
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
@ -89,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
|
||||
@ -122,7 +126,7 @@ and are used when Lemur creates the CSR for your certificates.
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_STATE = "CA"
|
||||
LEMUR_DEFAULT_STATE = "California"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_LOCATION
|
||||
@ -152,15 +156,16 @@ and are used when Lemur creates the CSR for your certificates.
|
||||
Notification Options
|
||||
--------------------
|
||||
|
||||
Lemur currently has very basic support for notifications. Notifications are sent to the certificate creator, owner and
|
||||
security team as specified by the `SECURITY_TEAM_EMAIL` configuration parameter.
|
||||
Lemur currently has very basic support for notifications. Currently only expiration notifications are supported. Actual notification
|
||||
is handled by the notification plugins that you have configured. Lemur ships with the 'Email' notification that allows expiration emails
|
||||
to be sent to subscribers.
|
||||
|
||||
The template for all of these notifications lives under lemur/template/event.html and can be easily modified to fit your
|
||||
needs.
|
||||
Templates for expiration emails are located under `lemur/plugins/lemur_email/templates` and can be modified for your needs.
|
||||
Notifications are sent to the certificate creator, owner and security team as specified by the `LEMUR_SECURITY_TEAM_EMAIL` configuration parameter.
|
||||
|
||||
Certificates marked as in-active will **not** be notified of upcoming expiration. This enables a user to essentially
|
||||
silence the expiration. If a certificate is active and is expiring the above will be notified at 30, 15, 5, 2 days
|
||||
respectively.
|
||||
silence the expiration. If a certificate is active and is expiring the above will be notified according to the `LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS` or
|
||||
30, 15, 2 days before expiration if no intervals are set.
|
||||
|
||||
Lemur supports sending certification expiration notifications through SES and SMTP.
|
||||
|
||||
@ -197,43 +202,67 @@ Lemur supports sending certification expiration notifications through SES and SM
|
||||
LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com']
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS
|
||||
:noindex:
|
||||
|
||||
Lemur notification intervals
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
|
||||
|
||||
|
||||
Authority Options
|
||||
-----------------
|
||||
|
||||
Authorities will each have their own configuration options. There are currently two plugins bundled with Lemur,
|
||||
Verisign/Symantec and CloudCA
|
||||
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 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
|
||||
:noindex:
|
||||
|
||||
This is the path to the mutual SSL certificate used for communicating with Verisign
|
||||
This is the path to the mutual TLS certificate used for communicating with Verisign
|
||||
|
||||
|
||||
.. data:: CLOUDCA_URL
|
||||
.. data:: VERISIGN_FIRST_NAME
|
||||
:noindex:
|
||||
|
||||
This is the URL for CLoudCA API
|
||||
This is the first name to be used when requesting the certificate
|
||||
|
||||
|
||||
.. data:: CLOUDCA_PEM_PATH
|
||||
.. data:: VERISIGN_LAST_NAME
|
||||
:noindex:
|
||||
|
||||
This is the path to the mutual SSL Certificate use for communicating with CLOUDCA
|
||||
This is the last name to be used when requesting the certificate
|
||||
|
||||
.. data:: CLOUDCA_BUNDLE
|
||||
.. data:: VERISIGN_EMAIL
|
||||
:noindex:
|
||||
|
||||
This is the path to the CLOUDCA certificate bundle
|
||||
This is the email to be used when requesting the certificate
|
||||
|
||||
|
||||
.. data:: VERISIGN_INTERMEDIATE
|
||||
:noindex:
|
||||
|
||||
This is the intermediate to be used for your CA chain
|
||||
|
||||
|
||||
.. data:: VERISIGN_ROOT
|
||||
:noindex:
|
||||
|
||||
This is the root to be used for your CA chain
|
||||
|
||||
|
||||
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>`_
|
||||
@ -272,22 +301,22 @@ 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 SSL certificates in AWS. But is not required to do so.
|
||||
.. note:: AWS usage is completely optional. Lemur can upload, find and manage TLS certificates in AWS. But is not required to do so.
|
||||
|
||||
Setting up IAM roles
|
||||
--------------------
|
||||
|
||||
Lemur's aws plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
|
||||
Lemur's AWS plugin uses boto heavily to talk to all the AWS resources it manages. By default it uses the on-instance credentials to make the necessary calls.
|
||||
|
||||
In order to limit the permissions we will create a new two IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
|
||||
In order to limit the permissions, we will create two new IAM roles for Lemur. You can name them whatever you would like but for example sake we will be calling them LemurInstanceProfile and Lemur.
|
||||
|
||||
Lemur uses to STS to talk to different accounts. For managing one account this isn't necessary but we will still use it so that we can easily add new accounts.
|
||||
|
||||
LemurInstanceProfile is the IAM role you will launch your instance with. It actually has almost no rights. In fact it should really only be able to use STS to assume role to the Lemur role.
|
||||
|
||||
Here is are example polices for the LemurInstanceProfile:
|
||||
Here are example policies for the LemurInstanceProfile:
|
||||
|
||||
SES-SendEmail
|
||||
|
||||
@ -325,7 +354,12 @@ STS-AssumeRole
|
||||
|
||||
|
||||
|
||||
Next we will create the the Lemur IAM role. Lemur
|
||||
Next we will create the the Lemur IAM role.
|
||||
|
||||
.. 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.
|
||||
|
||||
|
||||
Here is an example policy for Lemur:
|
||||
|
||||
@ -442,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.
|
||||
@ -451,7 +485,7 @@ Upgrading Lemur
|
||||
===============
|
||||
|
||||
Lemur provides an easy way to upgrade between versions. Simply download the newest
|
||||
version of Lemur from pypi and then apply any schema cahnges with the following command.
|
||||
version of Lemur from pypi and then apply any schema changes with the following command.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -524,29 +558,11 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
|
||||
lemur db upgrade
|
||||
|
||||
|
||||
.. data:: create_user
|
||||
|
||||
Creates new users within Lemur.
|
||||
|
||||
::
|
||||
|
||||
lemur create_user -u jim -e jim@example.com
|
||||
|
||||
|
||||
.. data:: create_role
|
||||
|
||||
Creates new roles within Lemur.
|
||||
|
||||
::
|
||||
|
||||
lemur create_role -n example -d "a new role"
|
||||
|
||||
|
||||
.. 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
|
||||
@ -566,27 +582,79 @@ All commands default to `~/.lemur/lemur.conf.py` if a configuration is not speci
|
||||
lemur sync -list
|
||||
|
||||
|
||||
Sub-commands
|
||||
------------
|
||||
|
||||
Lemur includes several sub-commands for interacting with Lemur such as creating new users, creating new roles and even
|
||||
issuing certificates.
|
||||
|
||||
The best way to discover these commands is by using the built in help pages
|
||||
|
||||
::
|
||||
|
||||
lemur --help
|
||||
|
||||
|
||||
and to get help on sub-commands
|
||||
|
||||
::
|
||||
|
||||
lemur certificates --help
|
||||
|
||||
|
||||
Identity and Access Management
|
||||
==============================
|
||||
|
||||
Lemur uses a Role Based Access Control (RBAC) mechanism to control which users have access to which resources. When a
|
||||
user is first created in Lemur the can be assigned one or more roles. These roles are typically dynamically created
|
||||
user is first created in Lemur they can be assigned one or more roles. These roles are typically dynamically created
|
||||
depending on a external identity provider (Google, LDAP, etc.,) or are hardcoded within Lemur and associated with special
|
||||
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
|
||||
private key.
|
||||
private key. Owners can also be a role name, such that any user with the same role as owner will be allowed to view the
|
||||
private key information.
|
||||
|
||||
These permissions are applied to the user upon login and refreshed on every request.
|
||||
|
||||
.. 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.
|
@ -1,2 +1 @@
|
||||
Change Log
|
||||
==========
|
||||
.. include:: ../CHANGELOG.rst
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# security_monkey documentation build configuration file, created by
|
||||
# lemur documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sat Jun 7 18:43:48 2014.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
@ -57,7 +57,7 @@ copyright = u'2015, Netflix Inc.'
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1.1'
|
||||
release = '0.1.3'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -102,7 +102,7 @@ pygments_style = 'sphinx'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
|
@ -1,20 +0,0 @@
|
||||
lemur_cloudca Package
|
||||
=====================
|
||||
|
||||
:mod:`lemur_cloudca` Package
|
||||
----------------------------
|
||||
|
||||
.. automodule:: lemur.plugins.lemur_cloudca
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
:mod:`plugin` Module
|
||||
--------------------
|
||||
|
||||
.. automodule:: lemur.plugins.lemur_cloudca.plugin
|
||||
:noindex:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
@ -8,7 +8,7 @@ Several interfaces exist for extending Lemur:
|
||||
* Source (lemur.plugins.base.source)
|
||||
* Notification (lemur.plugins.base.notification)
|
||||
|
||||
Each interface has its own function that will need to be defined in order for
|
||||
Each interface has its own functions that will need to be defined in order for
|
||||
your plugin to work correctly. See :ref:`Plugin Interfaces <PluginInterfaces>` for details.
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ Issuer
|
||||
Issuer plugins are used when you have an external service that creates certificates or authorities.
|
||||
In the simple case the third party only issues certificates (Verisign, DigiCert, etc.).
|
||||
|
||||
If you have a third party or internal service that creates authorities (CloudCA, EJBCA, etc.), Lemur has you covered,
|
||||
If you have a third party or internal service that creates authorities (EJBCA, etc.), Lemur has you covered,
|
||||
it can treat any issuer plugin as both a source of creating new certificates as well as new authorities.
|
||||
|
||||
|
||||
@ -215,7 +215,7 @@ certificate Lemur does not know about and adding the certificate to it's invento
|
||||
The `SourcePlugin` object has one default option of `pollRate`. This controls the number of seconds which to get new certificates.
|
||||
|
||||
.. warning::
|
||||
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarentee that ]
|
||||
Lemur currently has a very basic polling system of running a cron job every 15min to see which source plugins need to be run. A lock file is generated to guarantee that
|
||||
only one sync is running at a time. It also means that the minimum resolution of a source plugin poll rate is effectively 15min. You can always specify a faster cron
|
||||
job if you need a higher resolution sync job.
|
||||
|
||||
|
20
docs/faq.rst
@ -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
|
||||
--------
|
||||
|
||||
|
@ -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()
|
BIN
docs/guide/certificate_extensions.png
Normal file
After Width: | Height: | Size: 125 KiB |
BIN
docs/guide/create.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
docs/guide/create_authority.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
docs/guide/create_authority_options.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/guide/create_certificate.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
docs/guide/create_role.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/guide/create_user.png
Normal file
After Width: | Height: | Size: 39 KiB |
@ -1,14 +1,103 @@
|
||||
Creating Users
|
||||
==============
|
||||
User Guide
|
||||
==========
|
||||
|
||||
These guides are quick tutorials on how to perform basic tasks in Lemur.
|
||||
|
||||
|
||||
Creating Roles
|
||||
==============
|
||||
Create a New Authority
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Before Lemur can issue certificates you must configure the authority you wish use. Lemur itself does
|
||||
not issue certificates, it relies on external CAs and the plugins associated with those CAs to create the certificate
|
||||
that Lemur can then manage.
|
||||
|
||||
|
||||
Creating Authorities
|
||||
====================
|
||||
.. figure:: create.png
|
||||
|
||||
In the authority table select "Create"
|
||||
|
||||
.. figure:: create_authority.png
|
||||
|
||||
Enter a authority name and short description about the authority. Enter an owner,
|
||||
and certificate common name. Depending on the authority and the authority/issuer plugin
|
||||
these values may or may not be used.
|
||||
|
||||
.. figure:: create_authority_options.png
|
||||
|
||||
Again how many of these values get used largely depends on the underlying plugin. It
|
||||
is important to make sure you select the right plugin that you wish to use.
|
||||
|
||||
|
||||
Create a New Certificate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. figure:: create.png
|
||||
|
||||
In the certificate table select "Create"
|
||||
|
||||
.. figure:: create_certificate.png
|
||||
|
||||
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.
|
||||
|
||||
You can add notification options and upload the created certificate to a destination, both
|
||||
of these are editable features and can be changed after the certificate has been created.
|
||||
|
||||
.. figure:: certificate_extensions.png
|
||||
|
||||
These options are typically for advanced users, the one exception is the `Subject Alternate Names` or SAN.
|
||||
For certificates that need to include more than one domains, the first domain is the Common Name and all
|
||||
other domains are added here as DNSName entries.
|
||||
|
||||
|
||||
Import an Existing Certificate
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. figure:: upload_certificate.png
|
||||
|
||||
Enter a owner, short description and public certificate. If there are intermediates and private keys
|
||||
Lemur will track them just as it does if the certificate were created through Lemur. Lemur generates
|
||||
a certificate name but you can override that by passing a value to the `Custom Name` field.
|
||||
|
||||
You can add notification options and upload the created certificate to a destination, both
|
||||
of these are editable features and can be changed after the certificate has been created.
|
||||
|
||||
|
||||
Create a New User
|
||||
~~~~~~~~~~~~~~~~~
|
||||
.. figure:: settings.png
|
||||
|
||||
From the settings dropdown select "Users"
|
||||
|
||||
.. figure:: create.png
|
||||
|
||||
In the user table select "Create"
|
||||
|
||||
.. figure:: create_user.png
|
||||
|
||||
Enter the username, email and password for the user. You can also assign any
|
||||
roles that the user will need when they login. While there is no deletion
|
||||
(we want to track creators forever) you can mark a user as 'Inactive' that will
|
||||
not allow them to login to Lemur.
|
||||
|
||||
|
||||
Create a New Role
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. figure:: settings.png
|
||||
|
||||
From the settings dropdown select "Roles"
|
||||
|
||||
.. figure:: create.png
|
||||
|
||||
In the role table select "Create"
|
||||
|
||||
.. figure:: create_role.png
|
||||
|
||||
Enter a role name and short description about the role. You can optionally store
|
||||
a user/password on the role. This is useful if your authority require specific roles.
|
||||
You can then accurately map those roles onto Lemur users. Also optional you can assign
|
||||
users to your new role.
|
||||
|
||||
|
||||
Creating Certificates
|
||||
=====================
|
||||
|
BIN
docs/guide/settings.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/guide/upload_certificate.png
Normal file
After Width: | Height: | Size: 73 KiB |
@ -1,8 +1,8 @@
|
||||
Lemur
|
||||
=====
|
||||
|
||||
Lemur is a SSL management service. It attempts to help track and create certificates. By removing common issues with
|
||||
CSR creation it gives normal developers 'sane' SSL defaults and helps security teams push SSL usage throughout an organization.
|
||||
Lemur is a TLS management service. It attempts to help track and create certificates. By removing common issues with
|
||||
CSR creation it gives normal developers 'sane' TLS defaults and helps security teams push TLS usage throughout an organization.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
@ -6,21 +6,22 @@ There are several steps needed to make Lemur production ready. Here we focus on
|
||||
Basics
|
||||
======
|
||||
|
||||
Because of the sensitivity of the information stored and maintain by Lemur it is important that you follow standard host hardening practices:
|
||||
Because of the sensitivity of the information stored and maintained by Lemur it is important that you follow standard host hardening practices:
|
||||
|
||||
- Run Lemur with a limited user
|
||||
- Disabled any unneeded service
|
||||
- Disabled any unneeded services
|
||||
- Enable remote logging
|
||||
- Restrict access to host
|
||||
|
||||
.. _CredentialManagement:
|
||||
|
||||
Credential Management
|
||||
---------------------
|
||||
|
||||
Lemur often contains credentials such as mutual SSL keys that are used to communicate with third party resources and for encrypting stored secrets. Lemur comes with the ability
|
||||
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:
|
||||
|
||||
@ -30,7 +31,7 @@ and
|
||||
|
||||
``lemur unlock``
|
||||
|
||||
If you choose to use this feature ensure that the KEY are decrypted before Lemur starts as it will have trouble communicating with the database otherwise.
|
||||
If you choose to use this feature ensure that the keys are decrypted before Lemur starts as it will have trouble communicating with the database otherwise.
|
||||
|
||||
Entropy
|
||||
-------
|
||||
@ -56,8 +57,8 @@ For additional information about OpenSSL entropy issues:
|
||||
- `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_
|
||||
|
||||
|
||||
SSL
|
||||
====
|
||||
TLS/SSL
|
||||
=======
|
||||
|
||||
Nginx
|
||||
-----
|
||||
@ -71,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.
|
||||
@ -89,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::
|
||||
|
||||
@ -127,10 +128,10 @@ You can make some adjustments to get a better user experience::
|
||||
|
||||
}
|
||||
|
||||
This makes Nginx serve the favicon and static files which is is much better at than python.
|
||||
This makes Nginx serve the favicon and static files which it is much better at than python.
|
||||
|
||||
It is highly recommended that you deploy SSL when deploying Lemur. This may be obvious given Lemur's purpose but the
|
||||
sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates SSL::
|
||||
It is highly recommended that you deploy TLS when deploying Lemur. This may be obvious given Lemur's purpose but the
|
||||
sensitive nature of Lemur and what it controls makes this essential. This is a sample config for Lemur that also terminates TLS::
|
||||
|
||||
server_tokens off;
|
||||
add_header X-Frame-Options DENY;
|
||||
@ -218,7 +219,7 @@ An example apache config::
|
||||
...
|
||||
</VirtualHost>
|
||||
|
||||
Also included in the configurations above are several best practices when it comes to deploying SSL. Things like enabling
|
||||
Also included in the configurations above are several best practices when it comes to deploying TLS. Things like enabling
|
||||
HSTS, disabling vulnerable ciphers are all good ideas when it comes to deploying Lemur into a production environment.
|
||||
|
||||
.. note::
|
||||
@ -270,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::
|
||||
|
||||
@ -292,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.
|
||||
|
@ -4,6 +4,8 @@ Quickstart
|
||||
This guide will step you through setting up a Python-based virtualenv, installing the required packages, and configuring the basic web service.
|
||||
This guide assumes a clean Ubuntu 14.04 instance, commands may differ based on the OS and configuration being used.
|
||||
|
||||
Pressed for time? See the Lemur docker file on `Github <https://github.com/Netflix/lemur-docker>`_.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
@ -12,12 +14,13 @@ Some basic prerequisites which you'll need in order to run Lemur:
|
||||
* A UNIX-based operating system. We test on Ubuntu, develop on OS X
|
||||
* Python 2.7
|
||||
* PostgreSQL
|
||||
* Ngnix
|
||||
* Nginx
|
||||
|
||||
.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and SSL (ELB),
|
||||
.. note:: Lemur was built with in AWS in mind. This means that things such as databases (RDS), mail (SES), and TLS (ELB),
|
||||
are largely handled for us. Lemur does **not** require AWS to function. Our guides and documentation try to be
|
||||
be as generic as possible and are not intended to document every step of launching Lemur into a given environment.
|
||||
|
||||
|
||||
Setting up an Environment
|
||||
-------------------------
|
||||
|
||||
@ -50,24 +53,7 @@ dependencies::
|
||||
|
||||
And optionally if your database is going to be on the same host as the webserver::
|
||||
|
||||
$ sudo apt-get install postgres
|
||||
|
||||
|
||||
Installing Lemur
|
||||
----------------
|
||||
|
||||
Once you've got the environment setup, you can install Lemur and all its dependencies with
|
||||
the same command you used to grab virtualenv::
|
||||
|
||||
pip install -U lemur
|
||||
|
||||
Once everything is installed, you should be able to execute the Lemur CLI, via ``lemur``, and get something
|
||||
like the following:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ lemur
|
||||
usage: lemur [--config=/path/to/settings.py] [command] [options]
|
||||
$ sudo apt-get install postgresql
|
||||
|
||||
|
||||
Installing from Source
|
||||
@ -75,7 +61,14 @@ Installing from Source
|
||||
|
||||
If you're installing the Lemur source (e.g. from git), you'll also need to install **npm**.
|
||||
|
||||
Once your system is prepared, symlink your source into the virtualenv:
|
||||
Once your system is prepared, ensure that you are in the virtualenv:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ which python
|
||||
|
||||
|
||||
And then run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@ -108,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
|
||||
@ -128,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
|
||||
|
||||
@ -142,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.
|
||||
@ -168,8 +161,8 @@ Setup a Reverse Proxy
|
||||
---------------------
|
||||
|
||||
By default, Lemur runs on port 5000. Even if you change this, under normal conditions you won't be able to bind to
|
||||
port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we recommend
|
||||
you setup a simple web proxy.
|
||||
port 80. To get around this (and to avoid running Lemur as a privileged user, which you shouldn't), we need setup a
|
||||
simple web proxy. There are many different web servers you can use for this, we like and recommend Nginx.
|
||||
|
||||
Proxying with Nginx
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@ -194,12 +187,6 @@ You'll use the builtin HttpProxyModule within Nginx to handle proxying
|
||||
index index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /www/lemur/lemur/static/dist;
|
||||
include mime.types;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
See :doc:`../production/index` for more details on using Nginx.
|
||||
|
||||
|
||||
@ -251,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
|
||||
@ -267,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
|
||||
~~~~
|
||||
@ -283,7 +270,9 @@ Decrypts sensitive key material - Used to decrypt the secrets stored in source d
|
||||
What's Next?
|
||||
------------
|
||||
|
||||
The above 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
See :doc:`../production/index` for more details on how to configure Lemur for production.
|
||||
|
@ -3,3 +3,27 @@ Pygments>=1.2
|
||||
Sphinx>=1.3
|
||||
docutils>=0.7
|
||||
markupsafe
|
||||
sphinxcontrib-httpdomain
|
||||
Flask==0.10.1
|
||||
Flask-RESTful==0.3.3
|
||||
Flask-SQLAlchemy==2.0
|
||||
Flask-Script==2.0.5
|
||||
Flask-Migrate==1.4.0
|
||||
Flask-Bcrypt==0.6.2
|
||||
Flask-Principal==0.4.0
|
||||
Flask-Mail==0.9.1
|
||||
SQLAlchemy-Utils==0.30.11
|
||||
BeautifulSoup4
|
||||
requests==2.7.0
|
||||
psycopg2==2.6.1
|
||||
arrow==0.5.4
|
||||
boto==2.38.0 # we might make this optional
|
||||
six==1.9.0
|
||||
gunicorn==19.3.0
|
||||
pycrypto==2.6.1
|
||||
cryptography==1.0.1
|
||||
pyopenssl==0.15.1
|
||||
pyjwt==1.0.1
|
||||
xmltodict==0.9.2
|
||||
lockfile==0.10.2
|
||||
future==0.15.0
|
66
docs/security.rst
Normal 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
|
@ -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)
|
||||
|
@ -17,7 +17,7 @@ from lemur.domains.views import mod as domains_bp
|
||||
from lemur.destinations.views import mod as destinations_bp
|
||||
from lemur.authorities.views import mod as authorities_bp
|
||||
from lemur.certificates.views import mod as certificates_bp
|
||||
from lemur.status.views import mod as status_bp
|
||||
from lemur.defaults.views import mod as defaults_bp
|
||||
from lemur.plugins.views import mod as plugins_bp
|
||||
from lemur.notifications.views import mod as notifications_bp
|
||||
from lemur.sources.views import mod as sources_bp
|
||||
@ -31,7 +31,7 @@ LEMUR_BLUEPRINTS = (
|
||||
destinations_bp,
|
||||
authorities_bp,
|
||||
certificates_bp,
|
||||
status_bp,
|
||||
defaults_bp,
|
||||
plugins_bp,
|
||||
notifications_bp,
|
||||
sources_bp
|
||||
|
@ -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 \
|
||||
@ -125,7 +125,7 @@ class Ping(Resource):
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
# take the information we have received from Meechum to create a new request
|
||||
# take the information we have received from the provider to create a new request
|
||||
params = {
|
||||
'client_id': args['clientId'],
|
||||
'grant_type': 'authorization_code',
|
||||
@ -138,7 +138,7 @@ class Ping(Resource):
|
||||
access_token_url = current_app.config.get('PING_ACCESS_TOKEN_URL')
|
||||
user_api_url = current_app.config.get('PING_USER_API_URL')
|
||||
|
||||
# the secret and cliendId will be given to you when you signup for meechum
|
||||
# the secret and cliendId will be given to you when you signup for the provider
|
||||
basic = base64.b64encode('{0}:{1}'.format(args['clientId'], current_app.config.get("PING_SECRET")))
|
||||
headers = {'Authorization': 'Basic {0}'.format(basic)}
|
||||
|
||||
@ -220,7 +220,7 @@ class Ping(Resource):
|
||||
profile['email'],
|
||||
profile['email'],
|
||||
True,
|
||||
profile.get('thumbnailPhotoUrl'), # Encase profile isn't google+ enabled
|
||||
profile.get('thumbnailPhotoUrl'), # incase profile isn't google+ enabled
|
||||
roles
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -232,7 +232,7 @@ def create(**kwargs):
|
||||
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
|
||||
|
||||
# create default notifications for this certificate if none are provided
|
||||
notifications = []
|
||||
notifications = cert.notifications
|
||||
if not kwargs.get('notifications'):
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, [cert.owner])
|
||||
|
@ -7,7 +7,7 @@
|
||||
"""
|
||||
from builtins import str
|
||||
|
||||
from flask import Blueprint, current_app, make_response, jsonify
|
||||
from flask import Blueprint, make_response, jsonify
|
||||
from flask.ext.restful import reqparse, Api, fields
|
||||
|
||||
from cryptography import x509
|
||||
@ -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
|
||||
}
|
||||
@ -208,6 +209,46 @@ class CertificatesList(AuthenticatedResource):
|
||||
"notAfter": "2015-06-17T15:21:08",
|
||||
"description": "dsfdsf"
|
||||
},
|
||||
"notifications": [
|
||||
{
|
||||
"description": "Default 30 day expiration notification",
|
||||
"notificationOptions": [
|
||||
{
|
||||
"name": "interval",
|
||||
"required": true,
|
||||
"value": 30,
|
||||
"helpMessage": "Number of days to be alert before expiration.",
|
||||
"validation": "^\\d+$",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"available": [
|
||||
"days",
|
||||
"weeks",
|
||||
"months"
|
||||
],
|
||||
"name": "unit",
|
||||
"required": true,
|
||||
"value": "days",
|
||||
"helpMessage": "Interval unit",
|
||||
"validation": "",
|
||||
"type": "select"
|
||||
},
|
||||
{
|
||||
"name": "recipients",
|
||||
"required": true,
|
||||
"value": "bob@example.com",
|
||||
"helpMessage": "Comma delimited list of email addresses",
|
||||
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
|
||||
"type": "str"
|
||||
}
|
||||
],
|
||||
"label": "DEFAULT_KGLISSON_30_DAY",
|
||||
"pluginName": "email-notification",
|
||||
"active": true,
|
||||
"id": 7
|
||||
}
|
||||
],
|
||||
"extensions": {
|
||||
"basicConstraints": {},
|
||||
"keyUsage": {
|
||||
@ -276,18 +317,17 @@ class CertificatesList(AuthenticatedResource):
|
||||
self.reqparse.add_argument('extensions', type=dict, location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
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')
|
||||
self.reqparse.add_argument('authority', type=valid_authority, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('country', type=str, location='json')
|
||||
self.reqparse.add_argument('state', type=str, location='json')
|
||||
self.reqparse.add_argument('location', type=str, location='json')
|
||||
self.reqparse.add_argument('organization', type=str, location='json')
|
||||
self.reqparse.add_argument('organizationalUnit', type=str, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
self.reqparse.add_argument('commonName', 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)
|
||||
self.reqparse.add_argument('organization', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('organizationalUnit', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('owner', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('commonName', type=str, location='json', required=True)
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
@ -361,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"
|
||||
}
|
||||
@ -504,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"
|
||||
}
|
||||
@ -638,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"
|
||||
}
|
||||
@ -668,58 +711,9 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
return service.render(args)
|
||||
|
||||
|
||||
class CertificatesDefaults(AuthenticatedResource):
|
||||
""" Defineds the 'certificates' defaults endpoint """
|
||||
def __init__(self):
|
||||
super(CertificatesDefaults)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /certificates/defaults
|
||||
|
||||
Returns defaults needed to generate CSRs
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /certificates/defaults HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "Los Gatos",
|
||||
"organization": "Netflix",
|
||||
"organizationalUnit": "Operations"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return dict(
|
||||
country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
|
||||
state=current_app.config.get('LEMUR_DEFAULT_STATE'),
|
||||
location=current_app.config.get('LEMUR_DEFAULT_LOCATION'),
|
||||
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
|
||||
organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT')
|
||||
)
|
||||
|
||||
|
||||
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
|
||||
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
|
||||
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
|
||||
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
|
||||
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
|
||||
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates', endpoint='notificationCertificates')
|
||||
api.add_resource(CertificatesDefaults, '/certificates/defaults', endpoint='certificatesDefault')
|
||||
|
@ -22,6 +22,7 @@ class InstanceManager(object):
|
||||
|
||||
def add(self, class_path):
|
||||
self.cache = None
|
||||
if class_path not in self.class_list:
|
||||
self.class_list.append(class_path)
|
||||
|
||||
def remove(self, class_path):
|
||||
|
63
lemur/defaults/views.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
.. module: lemur.status.views
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from flask import current_app, Blueprint
|
||||
from flask.ext.restful import Api
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
|
||||
mod = Blueprint('default', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class LemurDefaults(AuthenticatedResource):
|
||||
""" Defines the 'defaults' endpoint """
|
||||
def __init__(self):
|
||||
super(LemurDefaults)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
.. http:get:: /defaults
|
||||
|
||||
Returns defaults needed to generate CSRs
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /defaults HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"country": "US",
|
||||
"state": "CA",
|
||||
"location": "Los Gatos",
|
||||
"organization": "Netflix",
|
||||
"organizationalUnit": "Operations"
|
||||
}
|
||||
|
||||
:reqheader Authorization: OAuth token to authenticate
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
return dict(
|
||||
country=current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
|
||||
state=current_app.config.get('LEMUR_DEFAULT_STATE'),
|
||||
location=current_app.config.get('LEMUR_DEFAULT_LOCATION'),
|
||||
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
|
||||
organizationalUnit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT')
|
||||
)
|
||||
|
||||
api.add_resource(LemurDefaults, '/defaults', endpoint='default')
|
@ -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 = []
|
||||
|
@ -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 = []
|
||||
@ -112,13 +112,6 @@ SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
|
||||
# These will be dependent on which 3rd party that Lemur is
|
||||
# configured to use.
|
||||
|
||||
# CLOUDCA_URL = ''
|
||||
# CLOUDCA_PEM_PATH = ''
|
||||
# CLOUDCA_BUNDLE = ''
|
||||
|
||||
# number of years to issue if not specified
|
||||
# CLOUDCA_DEFAULT_VALIDITY = 2
|
||||
|
||||
# VERISIGN_URL = ''
|
||||
# VERISIGN_PEM_PATH = ''
|
||||
# VERISIGN_FIRST_NAME = ''
|
||||
@ -178,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)),
|
||||
)
|
||||
@ -263,16 +258,21 @@ class InitializeApp(Command):
|
||||
Additionally a Lemur user will be created as a default user
|
||||
and be used when certificates are discovered by Lemur.
|
||||
"""
|
||||
def run(self):
|
||||
option_list = (
|
||||
Option('-p', '--password', dest='password'),
|
||||
)
|
||||
|
||||
def run(self, password):
|
||||
create()
|
||||
user = user_service.get_by_username("lemur")
|
||||
|
||||
if not user:
|
||||
if not password:
|
||||
sys.stdout.write("We need to set Lemur's password to continue!\n")
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
password = prompt_pass("Password")
|
||||
password1 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
if password != password1:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
|
||||
@ -285,16 +285,16 @@ class InitializeApp(Command):
|
||||
role = role_service.create('admin', description='this is the lemur administrator role')
|
||||
sys.stdout.write("[+] Created 'admin' role\n")
|
||||
|
||||
user_service.create("lemur", password1, 'lemur@nobody', True, None, [role])
|
||||
user_service.create("lemur", password, 'lemur@nobody', True, None, [role])
|
||||
sys.stdout.write("[+] Added a 'lemur' user and added it to the 'admin' role!\n")
|
||||
|
||||
else:
|
||||
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
|
||||
|
||||
sys.stdout.write("[+] Creating expiration email notifications!\n")
|
||||
sys.stdout.write("[!] Using {recipients} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n")
|
||||
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format("LEMUR_SECURITY_TEAM_EMAIL"))
|
||||
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [])
|
||||
sys.stdout.write(
|
||||
"[!] Creating {num} notifications for {intervals} days as specified by LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS\n".format(
|
||||
num=len(intervals),
|
||||
@ -316,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):
|
||||
@ -569,11 +569,11 @@ class ProvisionELB(Command):
|
||||
'authority': authority,
|
||||
'owner': owner,
|
||||
# defaults:
|
||||
'organization': u'Netflix, Inc.',
|
||||
'organizationalUnit': u'Operations',
|
||||
'country': u'US',
|
||||
'state': u'California',
|
||||
'location': u'Los Gatos'
|
||||
'organization': current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
|
||||
'organizationalUnit': current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
|
||||
'country': current_app.config.get('LEMUR_DEFAULT_COUNTRY'),
|
||||
'state': current_app.config.get('LEMUR_DEFAULT_STATE'),
|
||||
'location': current_app.config.get('LEMUR_DEFAULT_LOCATION')
|
||||
}
|
||||
|
||||
return options
|
||||
@ -718,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'))
|
||||
|
26
lemur/migrations/versions/4bcfa2c36623_.py
Normal 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 ###
|
255
lemur/migrations/versions/ed422fc58ba_.py
Normal 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')
|
@ -9,7 +9,6 @@
|
||||
|
||||
"""
|
||||
import ssl
|
||||
import socket
|
||||
|
||||
import arrow
|
||||
|
||||
@ -114,8 +113,9 @@ def _get_domain_certificate(name):
|
||||
try:
|
||||
pub_key = ssl.get_server_certificate((name, 443))
|
||||
return cert_service.find_duplicates(pub_key.strip())
|
||||
except socket.gaierror as e:
|
||||
except Exception as e:
|
||||
current_app.logger.info(str(e))
|
||||
return []
|
||||
|
||||
|
||||
def _find_superseded(cert):
|
||||
|
@ -1,5 +0,0 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
@ -1,364 +0,0 @@
|
||||
"""
|
||||
.. module: lemur.common.services.issuers.plugins.cloudca
|
||||
:platform: Unix
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
import re
|
||||
import ssl
|
||||
import base64
|
||||
from json import dumps
|
||||
|
||||
import arrow
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.exceptions import LemurException
|
||||
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
|
||||
from lemur.plugins import lemur_cloudca as cloudca
|
||||
|
||||
from lemur.authorities import service as authority_service
|
||||
|
||||
|
||||
class CloudCAException(LemurException):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
current_app.logger.error(self)
|
||||
|
||||
def __str__(self):
|
||||
return repr("CloudCA request failed: {0}".format(self.message))
|
||||
|
||||
|
||||
class CloudCAHostNameCheckingAdapter(HTTPAdapter):
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
super(CloudCAHostNameCheckingAdapter, self).cert_verify(conn, url, verify, cert)
|
||||
conn.assert_hostname = False
|
||||
|
||||
|
||||
def remove_none(options):
|
||||
"""
|
||||
Simple function that traverse the options and removed any None items
|
||||
CloudCA really dislikes null values.
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
new_dict = {}
|
||||
for k, v in options.items():
|
||||
if v:
|
||||
new_dict[k] = v
|
||||
|
||||
# this is super hacky and gross, cloudca doesn't like null values
|
||||
if new_dict.get('extensions'):
|
||||
if len(new_dict['extensions']['subAltNames']['names']) == 0:
|
||||
del new_dict['extensions']['subAltNames']
|
||||
|
||||
return new_dict
|
||||
|
||||
|
||||
def get_default_issuance(options):
|
||||
"""
|
||||
Gets the default time range for certificates
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
if not options.get('validityStart') and not options.get('validityEnd'):
|
||||
start = arrow.utcnow()
|
||||
options['validityStart'] = start.floor('second').isoformat()
|
||||
options['validityEnd'] = start.replace(years=current_app.config.get('CLOUDCA_DEFAULT_VALIDITY'))\
|
||||
.ceil('second').isoformat()
|
||||
return options
|
||||
|
||||
|
||||
def convert_to_pem(der):
|
||||
"""
|
||||
Converts DER to PEM Lemur uses PEM internally
|
||||
|
||||
:param der:
|
||||
:return:
|
||||
"""
|
||||
decoded = base64.b64decode(der)
|
||||
return ssl.DER_cert_to_PEM_cert(decoded)
|
||||
|
||||
|
||||
def convert_date_to_utc_time(date):
|
||||
"""
|
||||
Converts a python `datetime` object to the current date + current time in UTC.
|
||||
|
||||
:param date:
|
||||
:return:
|
||||
"""
|
||||
d = arrow.get(date)
|
||||
return arrow.utcnow().replace(day=d.naive.day).replace(month=d.naive.month).replace(year=d.naive.year)\
|
||||
.replace(microsecond=0)
|
||||
|
||||
|
||||
def process_response(response):
|
||||
"""
|
||||
Helper function that processes responses from CloudCA.
|
||||
|
||||
:param response:
|
||||
:return: :raise CloudCAException:
|
||||
"""
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if res['returnValue'] != 'success':
|
||||
current_app.logger.debug(res)
|
||||
if res.get('data'):
|
||||
raise CloudCAException(" ".join([res['returnMessage'], res['data']['dryRunResultMessage']]))
|
||||
else:
|
||||
raise CloudCAException(res['returnMessage'])
|
||||
else:
|
||||
raise CloudCAException("There was an error with your request: {0}".format(response.status_code))
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_auth_data(ca_name):
|
||||
"""
|
||||
Creates the authentication record needed to authenticate a user request to CloudCA.
|
||||
|
||||
:param ca_name:
|
||||
:return: :raise CloudCAException:
|
||||
"""
|
||||
role = authority_service.get_authority_role(ca_name)
|
||||
if role:
|
||||
return {
|
||||
"authInfo": {
|
||||
"credType": "password",
|
||||
"credentials": {
|
||||
"username": role.username,
|
||||
"password": role.password # we only decrypt when we need to
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
raise CloudCAException("You do not have the required role to issue certificates from {0}".format(ca_name))
|
||||
|
||||
|
||||
class CloudCA(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.session = requests.Session()
|
||||
self.session.mount('https://', CloudCAHostNameCheckingAdapter())
|
||||
self.url = current_app.config.get('CLOUDCA_URL')
|
||||
|
||||
if current_app.config.get('CLOUDCA_PEM_PATH') and current_app.config.get('CLOUDCA_BUNDLE'):
|
||||
self.session.cert = current_app.config.get('CLOUDCA_PEM_PATH')
|
||||
self.ca_bundle = current_app.config.get('CLOUDCA_BUNDLE')
|
||||
else:
|
||||
current_app.logger.warning(
|
||||
"No CLOUDCA credentials found, lemur will be unable to request certificates from CLOUDCA"
|
||||
)
|
||||
|
||||
super(CloudCA, self).__init__(*args, **kwargs)
|
||||
|
||||
def post(self, endpoint, data):
|
||||
"""
|
||||
HTTP POST to CloudCA
|
||||
|
||||
:param endpoint:
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
data = dumps(dict(data.items() + get_auth_data(data['caName']).items()))
|
||||
|
||||
# we set a low timeout, if cloudca is down it shouldn't bring down
|
||||
# lemur
|
||||
try:
|
||||
response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle)
|
||||
except ConnectionError:
|
||||
raise Exception("Could not talk to CloudCA, is it up?")
|
||||
|
||||
return process_response(response)
|
||||
|
||||
def get(self, endpoint):
|
||||
"""
|
||||
HTTP GET to CloudCA
|
||||
|
||||
:param endpoint:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
|
||||
except ConnectionError:
|
||||
raise Exception("Could not talk to CloudCA, is it up?")
|
||||
|
||||
return process_response(response)
|
||||
|
||||
def random(self, length=10):
|
||||
"""
|
||||
Uses CloudCA as a decent source of randomness.
|
||||
|
||||
:param length:
|
||||
:return:
|
||||
"""
|
||||
endpoint = '/v1/random/{0}'.format(length)
|
||||
response = self.session.get(self.url + endpoint, verify=self.ca_bundle)
|
||||
return response
|
||||
|
||||
def get_authorities(self):
|
||||
"""
|
||||
Retrieves authorities that were made outside of Lemur.
|
||||
|
||||
:return:
|
||||
"""
|
||||
endpoint = '{0}/listCAs'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
authorities = []
|
||||
for ca in self.get(endpoint)['data']['caList']:
|
||||
try:
|
||||
authorities.append(ca['caName'])
|
||||
except AttributeError:
|
||||
current_app.logger.error("No authority has been defined for {}".format(ca['caName']))
|
||||
|
||||
return authorities
|
||||
|
||||
|
||||
class CloudCAIssuerPlugin(IssuerPlugin, CloudCA):
|
||||
title = 'CloudCA'
|
||||
slug = 'cloudca-issuer'
|
||||
description = 'Enables the creation of certificates from the cloudca API.'
|
||||
version = cloudca.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
def create_authority(self, options):
|
||||
"""
|
||||
Creates a new certificate authority
|
||||
|
||||
:param options:
|
||||
:return:
|
||||
"""
|
||||
# this is weird and I don't like it
|
||||
endpoint = '{0}/createCA'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
options['caDN']['email'] = options['ownerEmail']
|
||||
|
||||
if options['caType'] == 'subca':
|
||||
options = dict(options.items() + self.auth_data(options['caParent']).items())
|
||||
|
||||
options['validityStart'] = convert_date_to_utc_time(options['validityStart']).isoformat()
|
||||
options['validityEnd'] = convert_date_to_utc_time(options['validityEnd']).isoformat()
|
||||
options['description'] = re.sub(r'[^a-zA-Z0-9]', '', options['caDescription'])
|
||||
|
||||
try:
|
||||
response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
except ConnectionError:
|
||||
raise Exception("Could not communicate with CloudCA, is it up?")
|
||||
|
||||
json = process_response(response)
|
||||
roles = []
|
||||
|
||||
for cred in json['data']['authInfo']:
|
||||
role = {
|
||||
'username': cred['credentials']['username'],
|
||||
'password': cred['credentials']['password'],
|
||||
'name': "_".join([options['caName'], cred['credentials']['username']])
|
||||
}
|
||||
roles.append(role)
|
||||
|
||||
if options['caType'] == 'subca':
|
||||
cert = convert_to_pem(json['data']['certificate'])
|
||||
else:
|
||||
cert = convert_to_pem(json['data']['rootCertificate'])
|
||||
|
||||
intermediates = []
|
||||
for i in json['data']['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
return cert, "".join(intermediates), roles,
|
||||
|
||||
def create_certificate(self, csr, options):
|
||||
"""
|
||||
Creates a new certificate from cloudca
|
||||
|
||||
If no start and end date are specified the default issue range
|
||||
will be used.
|
||||
|
||||
:param csr:
|
||||
:param options:
|
||||
"""
|
||||
endpoint = '{0}/enroll'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
# lets default to two years if it's not specified
|
||||
# we do some last minute data massaging
|
||||
options = get_default_issuance(options)
|
||||
|
||||
cloudca_options = {
|
||||
'extensions': options['extensions'],
|
||||
'validityStart': convert_date_to_utc_time(options['validityStart']).isoformat(),
|
||||
'validityEnd': convert_date_to_utc_time(options['validityEnd']).isoformat(),
|
||||
'creator': options['creator'],
|
||||
'ownerEmail': options['owner'],
|
||||
'caName': options['authority'].name,
|
||||
'csr': csr,
|
||||
'comment': re.sub(r'[^a-zA-Z0-9]', '', options['description'])
|
||||
}
|
||||
|
||||
response = self.post(endpoint, remove_none(cloudca_options))
|
||||
|
||||
# we return a concatenated list of intermediate because that is what aws
|
||||
# expects
|
||||
cert = convert_to_pem(response['data']['certificate'])
|
||||
|
||||
intermediates = [convert_to_pem(response['data']['rootCertificate'])]
|
||||
for i in response['data']['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
return cert, "".join(intermediates),
|
||||
|
||||
|
||||
class CloudCASourcePlugin(SourcePlugin, CloudCA):
|
||||
title = 'CloudCA'
|
||||
slug = 'cloudca-source'
|
||||
description = 'Discovers all SSL certificates in CloudCA'
|
||||
version = cloudca.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
|
||||
options = {
|
||||
'pollRate': {'type': 'int', 'default': '60'}
|
||||
}
|
||||
|
||||
def get_certificates(self, options, **kwargs):
|
||||
certs = []
|
||||
for authority in self.get_authorities():
|
||||
certs += self.get_cert(ca_name=authority)
|
||||
return certs
|
||||
|
||||
def get_cert(self, ca_name=None, cert_handle=None):
|
||||
"""
|
||||
Returns a given cert from CloudCA.
|
||||
|
||||
:param ca_name:
|
||||
:param cert_handle:
|
||||
:return:
|
||||
"""
|
||||
endpoint = '{0}/getCert'.format(current_app.config.get('CLOUDCA_API_ENDPOINT'))
|
||||
response = self.session.post(self.url + endpoint, data=dumps({'caName': ca_name}), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
raw = process_response(response)
|
||||
|
||||
certs = []
|
||||
for c in raw['data']['certList']:
|
||||
cert = convert_to_pem(c['certValue'])
|
||||
|
||||
intermediates = []
|
||||
for i in c['intermediateCertificates']:
|
||||
intermediates.append(convert_to_pem(i))
|
||||
|
||||
certs.append({
|
||||
'public_certificate': cert,
|
||||
'intermediate_certificate': "\n".join(intermediates),
|
||||
'owner': c['ownerEmail']
|
||||
})
|
||||
|
||||
return certs
|
@ -49,11 +49,11 @@
|
||||
<td class="container-padding" bgcolor="#ffffff" style="background-color: #ffffff; padding-left: 30px; padding-right: 30px; font-size: 14px; line-height: 20px; font-family: Helvetica, sans-serif; color: #333;">
|
||||
<br />
|
||||
<div style="font-weight: bold; font-size: 18px; line-height: 24px; color: #202d3b">
|
||||
<span style="color: #29abe0">Notice: Your SSL certificates are expiring!</span>
|
||||
<span style="color: #29abe0">Notice: Your TLS certificates are expiring!</span>
|
||||
<hr />
|
||||
</div>
|
||||
<p>
|
||||
Lemur, Netflix's SSL management portal has noticed that the following certificates are expiring soon, if you rely on these certificates
|
||||
Lemur, has noticed that the following certificates are expiring soon, if you rely on these certificates
|
||||
you should create new certificates to replace the certificates that are expiring.
|
||||
</p>
|
||||
<p>
|
||||
|
@ -1,57 +0,0 @@
|
||||
VERISIGN_INTERMEDIATE = """-----BEGIN CERTIFICATE-----
|
||||
MIIFFTCCA/2gAwIBAgIQKC4nkXkzkuQo8iGnTsk3rjANBgkqhkiG9w0BAQsFADCB
|
||||
yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL
|
||||
ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMTk5OSBWZXJp
|
||||
U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW
|
||||
ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0
|
||||
aG9yaXR5IC0gRzMwHhcNMTMxMDMxMDAwMDAwWhcNMjMxMDMwMjM1OTU5WjB+MQsw
|
||||
CQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNV
|
||||
BAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxLzAtBgNVBAMTJlN5bWFudGVjIENs
|
||||
YXNzIDMgU2VjdXJlIFNlcnZlciBDQSAtIEc0MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAstgFyhx0LbUXVjnFSlIJluhL2AzxaJ+aQihiw6UwU35VEYJb
|
||||
A3oNL+F5BMm0lncZgQGUWfm893qZJ4Itt4PdWid/sgN6nFMl6UgfRk/InSn4vnlW
|
||||
9vf92Tpo2otLgjNBEsPIPMzWlnqEIRoiBAMnF4scaGGTDw5RgDMdtLXO637QYqzu
|
||||
s3sBdO9pNevK1T2p7peYyo2qRA4lmUoVlqTObQJUHypqJuIGOmNIrLRM0XWTUP8T
|
||||
L9ba4cYY9Z/JJV3zADreJk20KQnNDz0jbxZKgRb78oMQw7jW2FUyPfG9D72MUpVK
|
||||
Fpd6UiFjdS8W+cRmvvW1Cdj/JwDNRHxvSz+w9wIDAQABo4IBQDCCATwwHQYDVR0O
|
||||
BBYEFF9gz2GQVd+EQxSKYCqy9Xr0QxjvMBIGA1UdEwEB/wQIMAYBAf8CAQAwawYD
|
||||
VR0gBGQwYjBgBgpghkgBhvhFAQc2MFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cu
|
||||
c3ltYXV0aC5jb20vY3BzMCgGCCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0
|
||||
aC5jb20vcnBhMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9zLnN5bWNiLmNvbS9w
|
||||
Y2EzLWczLmNybDAOBgNVHQ8BAf8EBAMCAQYwKQYDVR0RBCIwIKQeMBwxGjAYBgNV
|
||||
BAMTEVN5bWFudGVjUEtJLTEtNTM0MC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcw
|
||||
AYYSaHR0cDovL3Muc3ltY2QuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBbF1K+1lZ7
|
||||
9Pc0CUuWysf2IdBpgO/nmhnoJOJ/2S9h3RPrWmXk4WqQy04q6YoW51KN9kMbRwUN
|
||||
gKOomv4p07wdKNWlStRxPA91xQtzPwBIZXkNq2oeJQzAAt5mrL1LBmuaV4oqgX5n
|
||||
m7pSYHPEFfe7wVDJCKW6V0o6GxBzHOF7tpQDS65RsIJAOloknO4NWF2uuil6yjOe
|
||||
soHCL47BJ89A8AShP/U3wsr8rFNtqVNpT+F2ZAwlgak3A/I5czTSwXx4GByoaxbn
|
||||
5+CdKa/Y5Gk5eZVpuXtcXQGc1PfzSEUTZJXXCm5y2kMiJG8+WnDcwJLgLeVX+OQr
|
||||
J+71/xuzAYN6
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
||||
|
||||
VERISIGN_ROOT = """-----BEGIN CERTIFICATE-----
|
||||
MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
|
||||
CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
|
||||
cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
|
||||
LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
|
||||
aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
||||
dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
|
||||
VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
|
||||
aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
|
||||
bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
|
||||
IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
|
||||
LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b
|
||||
N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
|
||||
KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
|
||||
kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
|
||||
CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
|
||||
Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
|
||||
imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te
|
||||
2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe
|
||||
DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
|
||||
/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
|
||||
F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt
|
||||
TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
@ -13,9 +13,8 @@ import xmltodict
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from lemur.plugins.bases import IssuerPlugin
|
||||
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
|
||||
from lemur.plugins import lemur_verisign as verisign
|
||||
from lemur.plugins.lemur_verisign import constants
|
||||
from lemur.common.utils import get_psuedo_random_string
|
||||
|
||||
|
||||
@ -132,7 +131,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
|
||||
version = verisign.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur'
|
||||
author_url = 'https://github.com/netflix/lemur.git'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.session = requests.Session()
|
||||
@ -147,7 +146,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
|
||||
:param issuer_options:
|
||||
:return: :raise Exception:
|
||||
"""
|
||||
url = current_app.config.get("VERISIGN_URL") + '/enroll'
|
||||
url = current_app.config.get("VERISIGN_URL") + '/rest/services/enroll'
|
||||
|
||||
data = process_options(issuer_options)
|
||||
data['csr'] = csr
|
||||
@ -156,7 +155,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
|
||||
|
||||
response = self.session.post(url, data=data)
|
||||
cert = handle_response(response.content)['Response']['Certificate']
|
||||
return cert, constants.VERISIGN_INTERMEDIATE,
|
||||
return cert, current_app.config.get('VERISIGN_INTERMEDIATE'),
|
||||
|
||||
@staticmethod
|
||||
def create_authority(options):
|
||||
@ -168,7 +167,7 @@ class VerisignIssuerPlugin(IssuerPlugin):
|
||||
:return:
|
||||
"""
|
||||
role = {'username': '', 'password': '', 'name': 'verisign'}
|
||||
return constants.VERISIGN_ROOT, "", [role]
|
||||
return current_app.config.get('VERISIGN_ROOT'), "", [role]
|
||||
|
||||
def get_available_units(self):
|
||||
"""
|
||||
@ -177,6 +176,35 @@ class VerisignIssuerPlugin(IssuerPlugin):
|
||||
|
||||
:return:
|
||||
"""
|
||||
url = current_app.config.get("VERISIGN_URL") + '/getTokens'
|
||||
url = current_app.config.get("VERISIGN_URL") + '/rest/services/getTokens'
|
||||
response = self.session.post(url, headers={'content-type': 'application/x-www-form-urlencoded'})
|
||||
return handle_response(response.content)['Response']['Order']
|
||||
|
||||
|
||||
class VerisignSourcePlugin(SourcePlugin):
|
||||
title = 'Verisign'
|
||||
slug = 'verisign-source'
|
||||
description = 'Allows for the polling of issued certificates from the VICE2.0 verisign API.'
|
||||
version = verisign.VERSION
|
||||
|
||||
author = 'Kevin Glisson'
|
||||
author_url = 'https://github.com/netflix/lemur.git'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.session = requests.Session()
|
||||
self.session.cert = current_app.config.get('VERISIGN_PEM_PATH')
|
||||
super(VerisignSourcePlugin, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_certificates(self):
|
||||
url = current_app.config.get('VERISIGN_URL') + '/reportingws'
|
||||
end = arrow.now()
|
||||
start = end.replace(years=-5)
|
||||
data = {
|
||||
'reportType': 'detail',
|
||||
'startDate': start.format("MM/DD/YYYY"),
|
||||
'endDate': end.format("MM/DD/YYYY"),
|
||||
'structuredRecord': 'Y',
|
||||
'certStatus': 'Valid',
|
||||
}
|
||||
current_app.logger.debug(data)
|
||||
response = self.session.post(url, data=data)
|
||||
|
1
lemur/plugins/lemur_verisign/tests/conftest.py
Normal file
@ -0,0 +1 @@
|
||||
from lemur.tests.conftest import * # noqa
|
5
lemur/plugins/lemur_verisign/tests/test_verisign.py
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
def test_get_certificates(app):
|
||||
from lemur.plugins.base import plugins
|
||||
p = plugins.get('verisign-source')
|
||||
p.get_certificates()
|
@ -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):
|
||||
|
@ -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'))
|
||||
|
27
lemur/static/app/angular/app.js
vendored
@ -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({
|
||||
@ -60,6 +62,15 @@ lemur.controller('datePickerController', function ($scope, $timeout){
|
||||
};
|
||||
});
|
||||
|
||||
lemur.service('DefaultService', function (LemurRestangular) {
|
||||
var DefaultService = this;
|
||||
DefaultService.get = function () {
|
||||
return LemurRestangular.all('defaults').customGET().then(function (defaults) {
|
||||
return defaults;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
lemur.factory('LemurRestangular', function (Restangular, $location, $auth) {
|
||||
return Restangular.withConfig(function (RestangularConfigurer) {
|
||||
RestangularConfigurer.setBaseUrl('http://localhost:5000/api/1');
|
||||
|
@ -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'
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -30,6 +30,9 @@ angular.module('lemur')
|
||||
.controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler) {
|
||||
$scope.authority = LemurRestangular.restangularizeElement(null, {}, 'authorities');
|
||||
|
||||
// set the defaults
|
||||
AuthorityService.getDefaults($scope.authority);
|
||||
|
||||
$scope.loading = false;
|
||||
$scope.create = function (authority) {
|
||||
WizardHandler.wizard().context.loading = true;
|
||||
|
@ -10,7 +10,7 @@
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="authority.owner" placeholder="owner@netflix.com"
|
||||
<input type="email" name="owner" ng-model="authority.owner" placeholder="owner@example.com"
|
||||
class="form-control" required/>
|
||||
|
||||
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||
|
@ -16,7 +16,7 @@
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="ownerEmail" ng-model="authority.ownerEmail" placeholder="TeamDL@netflix.com" tooltip="This is the authorities team distribution list or the main point of contact for this authority" class="form-control" required/>
|
||||
<input type="email" name="ownerEmail" ng-model="authority.ownerEmail" placeholder="TeamDL@example.com" tooltip="This is the authorities team distribution list or the main point of contact for this authority" class="form-control" required/>
|
||||
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate Authority owner</p>
|
||||
</div>
|
||||
</div>
|
||||
|
12
lemur/static/app/angular/authorities/services.js
vendored
@ -56,7 +56,7 @@ angular.module('lemur')
|
||||
});
|
||||
return LemurRestangular.all('authorities');
|
||||
})
|
||||
.service('AuthorityService', function ($location, AuthorityApi, toaster) {
|
||||
.service('AuthorityService', function ($location, AuthorityApi, DefaultService, toaster) {
|
||||
var AuthorityService = this;
|
||||
AuthorityService.findAuthorityByName = function (filterValue) {
|
||||
return AuthorityApi.getList({'filter[name]': filterValue})
|
||||
@ -117,6 +117,16 @@ angular.module('lemur')
|
||||
});
|
||||
};
|
||||
|
||||
AuthorityService.getDefaults = function (authority) {
|
||||
return DefaultService.get().then(function (defaults) {
|
||||
authority.caDN.country = defaults.country;
|
||||
authority.caDN.state = defaults.state;
|
||||
authority.caDN.location = defaults.location;
|
||||
authority.caDN.organization = defaults.organization;
|
||||
authority.caDN.organizationalUnit = defaults.organizationalUnit;
|
||||
});
|
||||
};
|
||||
|
||||
AuthorityService.getRoles = function (authority) {
|
||||
return authority.getList('roles').then(function (roles) {
|
||||
authority.roles = roles;
|
||||
|
@ -2,15 +2,22 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/authorities', {
|
||||
.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
|
||||
|
@ -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">
|
||||
|
@ -4,7 +4,7 @@
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="certificate.selectedDestination" placeholder="AWS, TheSecret..."
|
||||
<input type="text" ng-model="certificate.selectedDestination" placeholder="AWS..."
|
||||
typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
|
||||
class="form-control input-md" typeahead-on-select="certificate.attachDestination($item)" typeahead-min-wait="50"
|
||||
tooltip="Lemur can upload certificates to any pre-defined destination"
|
||||
|
@ -10,7 +10,7 @@
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@netflix.com"
|
||||
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@example.com"
|
||||
class="form-control" required/>
|
||||
|
||||
<p ng-show="editForm.owner.$invalid && !editForm.owner.$pristine" class="help-block">Enter a valid
|
||||
|
@ -6,7 +6,7 @@
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@netflix.com" tooltip="This is the certificates team distribution list or main point of contact" class="form-control" required/>
|
||||
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@example.com" tooltip="This is the certificates team distribution list or main point of contact" class="form-control" required/>
|
||||
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate owner</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,7 +11,7 @@
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@netflix.com"
|
||||
<input type="email" name="owner" ng-model="certificate.owner" placeholder="owner@example.com"
|
||||
class="form-control" required/>
|
||||
|
||||
<p ng-show="uploadForm.owner.$invalid && !uploadForm.owner.$pristine" class="help-block">Enter a valid
|
||||
@ -24,7 +24,7 @@
|
||||
Custom Name <span class="glyphicon glyphicon-question-sign"></span>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="name" ng-model="certificate.name" placeholder="example.netflix.net-SymantecCorporation-20150828-20160830" class="form-control"/>
|
||||
<input name="name" ng-model="certificate.name" placeholder="the.example.net-SymantecCorporation-20150828-20160830" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
|
@ -89,7 +89,7 @@ angular.module('lemur')
|
||||
});
|
||||
return LemurRestangular.all('certificates');
|
||||
})
|
||||
.service('CertificateService', function ($location, CertificateApi, LemurRestangular, toaster) {
|
||||
.service('CertificateService', function ($location, CertificateApi, LemurRestangular, DefaultService, toaster) {
|
||||
var CertificateService = this;
|
||||
CertificateService.findCertificatesByName = function (filterValue) {
|
||||
return CertificateApi.getList({'filter[name]': filterValue})
|
||||
@ -207,7 +207,7 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
CertificateService.getDefaults = function (certificate) {
|
||||
return certificate.customGET('defaults').then(function (defaults) {
|
||||
return DefaultService.get().then(function (defaults) {
|
||||
certificate.country = defaults.country;
|
||||
certificate.state = defaults.state;
|
||||
certificate.location = defaults.location;
|
||||
|
@ -2,15 +2,23 @@
|
||||
|
||||
angular.module('lemur')
|
||||
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/certificates', {
|
||||
.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
|
||||
|
@ -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,9 +42,14 @@
|
||||
<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 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>
|
||||
@ -51,7 +57,8 @@
|
||||
<tr class="warning" ng-show="certificate.toggle" ng-repeat-end>
|
||||
<td colspan="6">
|
||||
<tabset justified="true" class="col-md-6">
|
||||
<tab heading="Basic Info">
|
||||
<tab>
|
||||
<tab-heading>Basic Info</tab-heading>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<strong>Creator</strong>
|
||||
@ -82,11 +89,17 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -100,7 +113,8 @@
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab heading="Notifications">
|
||||
<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>
|
||||
@ -108,7 +122,8 @@
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab heading="Destinations">
|
||||
<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>
|
||||
@ -116,30 +131,41 @@
|
||||
</li>
|
||||
</ul>
|
||||
</tab>
|
||||
<tab heading="Domains">
|
||||
<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>
|
||||
<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>
|
||||
|
14
lemur/static/app/angular/dashboard/dashboard.js
vendored
@ -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;
|
||||
});
|
||||
});
|
||||
|
@ -10,7 +10,8 @@
|
||||
<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>
|
||||
@ -22,7 +23,7 @@
|
||||
<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>
|
||||
<canvas id="issuersPie" class="chart chart-pie" data="issuers.values" labels="issuers.labels" colours="colours"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,7 +33,7 @@
|
||||
<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>
|
||||
<canvas id="bitsPie" class="chart chart-pie" data="bits.values" labels="bits.labels" colours="colours"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -43,10 +44,20 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
</div>
|
||||
|
@ -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'
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
|
5
lemur/static/app/angular/roles/view/view.js
vendored
@ -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'
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
|
5
lemur/static/app/angular/users/view/view.js
vendored
@ -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'
|
||||
});
|
||||
|
@ -1,19 +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 SSL 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">SSL In The Cloud <span class="text-muted">Encrypt it all </span></h2>
|
||||
|
||||
<p class="lead">The Security Operations team manages all of the SSL certificate generation at Netflix. This
|
||||
portal was created to serve as both a self service application so that application owners can provision
|
||||
their own certificates and to help enforce some key naming and security conventions, in order provide
|
||||
Netflix with scalable and manageable SSL security.</p>
|
||||
|
||||
<p>See <a href="http://go/ssl">go/ssl</a> for more info.</p>
|
||||
</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 »</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 »</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 »</a></p>
|
||||
</div><!-- /.col-lg-4 -->
|
||||
</div><!-- /.row -->
|
||||
</div>
|
||||
|
@ -49,29 +49,34 @@
|
||||
</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">
|
||||
<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>
|
||||
@ -83,11 +88,14 @@
|
||||
</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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
"""
|
||||
.. module: lemur.status.views
|
||||
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
|
||||
from flask import app, Blueprint, jsonify
|
||||
from flask.ext.restful import Api
|
||||
|
||||
from lemur.auth.service import AuthenticatedResource
|
||||
|
||||
|
||||
mod = Blueprint('status', __name__)
|
||||
api = Api(mod)
|
||||
|
||||
|
||||
class Status(AuthenticatedResource):
|
||||
""" Defines the 'accounts' endpoint """
|
||||
def __init__(self):
|
||||
super(Status, self).__init__()
|
||||
|
||||
def get(self):
|
||||
if not os.path.isdir(os.path.join(app.config.get("KEY_PATH"), "decrypted")):
|
||||
return jsonify({
|
||||
'environment': app.config.get('ENVIRONMENT'),
|
||||
'status': 'degraded',
|
||||
'message': "This Lemur instance is in a degraded state and is unable to issue certificates, please alert secops@netflix.com"})
|
||||
else:
|
||||
return jsonify({
|
||||
'environment': app.config.get('ENVIRONMENT'),
|
||||
'status': 'healthy',
|
||||
'message': "This Lemur instance is healthy"})
|
@ -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 = []
|
||||
|
@ -8,6 +8,7 @@ from lemur.roles import service as role_service
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--lemurconfig", help="override the default test config")
|
||||
parser.addoption("--runslow", action="store_true", help="run slow tests")
|
||||
|
||||
|
||||
@ -29,11 +30,14 @@ def pytest_runtest_makereport(item, call):
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope="session")
|
||||
def app():
|
||||
def app(request):
|
||||
"""
|
||||
Creates a new Flask application for a test duration.
|
||||
Uses application factory `create_app`.
|
||||
"""
|
||||
if request.config.getoption('--lemurconfig'):
|
||||
_app = create_app(request.config.getoption('--lemurconfig'))
|
||||
else:
|
||||
_app = create_app(os.path.dirname(os.path.realpath(__file__)) + '/conf.py')
|
||||
ctx = _app.app_context()
|
||||
ctx.push()
|
||||
|
@ -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():
|
||||
|
@ -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)
|
||||
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "Lemur",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -10,3 +10,6 @@ exclude = .tox,.git,*/migrations/*,lemur/static/*,docs/*
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
description-file = README.rst
|
||||
|
36
setup.py
@ -2,21 +2,23 @@
|
||||
Lemur
|
||||
=====
|
||||
|
||||
Is an SSL management and orchestration tool.
|
||||
Is a TLS management and orchestration tool.
|
||||
|
||||
:copyright: (c) 2015 by Netflix, see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import os.path
|
||||
import datetime
|
||||
|
||||
from distutils import log
|
||||
from distutils.core import Command
|
||||
from setuptools.command.develop import develop
|
||||
from setuptools.command.install import install
|
||||
from setuptools.command.sdist import sdist
|
||||
from setuptools import setup
|
||||
from setuptools import setup, find_packages
|
||||
from subprocess import check_output
|
||||
|
||||
ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__)))
|
||||
@ -39,7 +41,7 @@ install_requires = [
|
||||
'six==1.9.0',
|
||||
'gunicorn==19.3.0',
|
||||
'pycrypto==2.6.1',
|
||||
'cryptography==1.0',
|
||||
'cryptography==1.0.2',
|
||||
'pyopenssl==0.15.1',
|
||||
'pyjwt==1.0.1',
|
||||
'xmltodict==0.9.2',
|
||||
@ -87,9 +89,18 @@ class DevelopWithBuildStatic(develop):
|
||||
|
||||
|
||||
class SdistWithBuildStatic(sdist):
|
||||
def make_distribution(self):
|
||||
def make_release_tree(self, *a, **kw):
|
||||
dist_path = self.distribution.get_fullname()
|
||||
|
||||
sdist.make_release_tree(self, *a, **kw)
|
||||
|
||||
self.reinitialize_command('build_static', work_path=dist_path)
|
||||
self.run_command('build_static')
|
||||
return sdist.make_distribution(self)
|
||||
|
||||
with open(os.path.join(dist_path, 'lemur-package.json'), 'w') as fp:
|
||||
json.dump({
|
||||
'createdAt': datetime.datetime.utcnow().isoformat() + 'Z',
|
||||
}, fp)
|
||||
|
||||
|
||||
class BuildStatic(Command):
|
||||
@ -100,21 +111,27 @@ class BuildStatic(Command):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
log.info("running [npm install --quiet]")
|
||||
log.info("running [npm install --quiet] in {0}".format(ROOT))
|
||||
try:
|
||||
check_output(['npm', 'install', '--quiet'], cwd=ROOT)
|
||||
|
||||
log.info("running [gulp build]")
|
||||
check_output([os.path.join(ROOT, 'node_modules', '.bin', 'gulp'), 'build'], cwd=ROOT)
|
||||
log.info("running [gulp package]")
|
||||
check_output([os.path.join(ROOT, 'node_modules', '.bin', 'gulp'), 'package'], cwd=ROOT)
|
||||
except Exception as e:
|
||||
log.warn("Unable to build static content")
|
||||
|
||||
setup(
|
||||
name='lemur',
|
||||
version='0.1',
|
||||
version='0.1.5',
|
||||
author='Kevin Glisson',
|
||||
author_email='kglisson@netflix.com',
|
||||
url='https://github.com/netflix/lemur',
|
||||
download_url='https://github.com/Netflix/lemur/archive/0.1.3.tar.gz',
|
||||
description='Certificate management and orchestration service',
|
||||
long_description=open(os.path.join(ROOT, 'README.rst')).read(),
|
||||
packages=['lemur'],
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=install_requires,
|
||||
@ -127,7 +144,6 @@ setup(
|
||||
'build_static': BuildStatic,
|
||||
'sdist': SdistWithBuildStatic,
|
||||
'install': SmartInstall
|
||||
|
||||
},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
@ -135,8 +151,6 @@ setup(
|
||||
],
|
||||
'lemur.plugins': [
|
||||
'verisign_issuer = lemur.plugins.lemur_verisign.plugin:VerisignIssuerPlugin',
|
||||
'cloudca_issuer = lemur.plugins.lemur_cloudca.plugin:CloudCAIssuerPlugin',
|
||||
'cloudca_source = lemur.plugins.lemur_cloudca.plugin:CloudCASourcePlugin',
|
||||
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
|
||||
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin',
|
||||
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin',
|
||||
|