Compare commits
166 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
c182055dbe | |||
97a289f02a | |||
089c0b2b1b | |||
3b109ec578 | |||
45158c64a2 | |||
fe7b075f7b | |||
a350940cd1 | |||
efec79d8de | |||
62950128a2 | |||
aca69ce03c | |||
bf8ce354e5 | |||
8d09d865b1 | |||
8848146e8d | |||
480078da42 | |||
46a5355377 | |||
3fb226ec11 | |||
7471984ecf | |||
df9b345541 | |||
d75c641848 | |||
a484a6e24d | |||
a7fd74396c | |||
8977c5ddbf | |||
bbb63b4aa6 | |||
f492e9ec1b | |||
03e2991ced | |||
80136834b5 | |||
3b2f71cc8a | |||
572c44b78b | |||
783acf6d8c | |||
fc22f76708 | |||
6ec5d26f0c | |||
53ce9cac4c | |||
51800d5e4b | |||
627b36d2a5 | |||
70ccd137e1 | |||
9a04371680 | |||
f799ff3af1 | |||
6db1d0b031 | |||
d599aaa410 | |||
09bc79ef84 | |||
6e39a1e666 | |||
bb51b59400 | |||
75de814b15 | |||
b4c348aef7 | |||
3476d3bcf3 | |||
45c442000e | |||
a07db5625b | |||
3df50f15f7 | |||
4b7a55c89f | |||
3ff5cdf43f | |||
dbfd6b1e17 | |||
4b9a05198c | |||
d62f57eab3 | |||
96c3ab7f9d | |||
38ebeab163 | |||
fcfaa21a24 | |||
0f0d11a828 | |||
6b2da2fe6b | |||
74525e8e8e | |||
cbcc8af3bd | |||
ab7b0c442c | |||
39c022dbf3 | |||
b00917aa60 | |||
4a0328cd8f | |||
b96af3a1f1 | |||
28e12a973f | |||
1883f3c0e7 | |||
c6747439fb | |||
0b9c814ea5 | |||
f09f5eb0f1 | |||
95ac5245e1 | |||
dd607e5c07 | |||
eb55d5465f | |||
500b212a25 | |||
7554a86d23 | |||
43d4dbbfbd | |||
90e49613f9 | |||
d3ff79d800 | |||
bfcbd1b065 | |||
b488c349e8 | |||
590f43297f | |||
b8720566d7 | |||
d0d3e06c81 | |||
48f38a8625 | |||
13f34fc600 | |||
f679392c61 | |||
f78e9d47d1 | |||
92a3c1a5a0 |
2
.bowerrc
@ -1,3 +1,3 @@
|
||||
{
|
||||
"directory": "lemur/static/app/vendor/bower_components"
|
||||
"directory": "bower_components"
|
||||
}
|
||||
|
3
AUTHORS
@ -1 +1,2 @@
|
||||
- Kevin Glisson (kglisson@netflix.com)
|
||||
- Kevin Glisson <kglisson@netflix.com>
|
||||
- Jeremy Heffner <jheffner@netflix.com>
|
||||
|
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 *~
|
4
Makefile
@ -6,11 +6,11 @@ develop: update-submodules setup-git
|
||||
npm install
|
||||
pip install "setuptools>=0.9.8"
|
||||
# order matters here, base package must install first
|
||||
# this is temporary until the version we need is released
|
||||
pip install -e 'git+https://git@github.com/pyca/cryptography.git#egg=cryptography-1.0.dev1'
|
||||
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:
|
||||
|
23
README.rst
@ -1,6 +1,10 @@
|
||||
Lemur
|
||||
=====
|
||||
|
||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/Netflix/lemur
|
||||
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/lemur.svg
|
||||
:target: https://pypi.python.org/pypi/lemur/
|
||||
:alt: Latest Version
|
||||
@ -9,23 +13,20 @@ 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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults.
|
||||
|
||||
It works on CPython 2.7. It is known
|
||||
to work on Ubuntu Linux and OS X.
|
||||
It works on CPython 2.7, 3.3, 3.4. We deploy on Ubuntu and develop on 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>`_
|
||||
|
||||
|
||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||
:alt: Join the chat at https://gitter.im/Netflix/lemur
|
||||
:target: https://gitter.im/Netflix/lemur?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
- `Docker <https://github.com/Netflix/lemur-docker>`_
|
||||
|
@ -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.
|
||||
@ -74,8 +74,6 @@ Basic Configuration
|
||||
|
||||
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.
|
||||
|
||||
::
|
||||
|
||||
LEMUR_TOKEN_SECRET = 'supersecret'
|
||||
@ -102,18 +100,66 @@ Basic Configuration
|
||||
LEMUR_ENCRYPTION_KEY = 'supersupersecret'
|
||||
|
||||
|
||||
Certificate Default Options
|
||||
---------------------------
|
||||
|
||||
Lemur allows you to find tune your certificates to your organization. The following defaults are presented in the UI
|
||||
and are used when Lemur creates the CSR for your certificates.
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_COUNTRY
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_COUNTRY = "US"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_STATE
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_STATE = "California"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_LOCATION
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_LOCATION = "Los Gatos"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATION
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_ORGANIZATION = "Netflix"
|
||||
|
||||
|
||||
.. data:: LEMUR_DEFAULT_ORGANIZATION_UNIT
|
||||
:noindex:
|
||||
|
||||
::
|
||||
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = "Operations"
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@ -150,11 +196,22 @@ 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 plugins own documentation
|
||||
for those plugins.
|
||||
|
||||
.. data:: VERISIGN_URL
|
||||
:noindex:
|
||||
@ -165,29 +222,44 @@ Verisign/Symantec and CloudCA
|
||||
.. 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, additional flows can be added relatively easily
|
||||
If you are not using PING you do not need to configure any of these options
|
||||
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>`_
|
||||
|
||||
.. data:: PING_SECRET
|
||||
:noindex:
|
||||
@ -225,20 +297,20 @@ 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.
|
||||
|
||||
.. 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
|
||||
|
||||
@ -278,6 +350,11 @@ STS-AssumeRole
|
||||
|
||||
Next we will create the the Lemur IAM role. Lemur
|
||||
|
||||
..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:
|
||||
|
||||
IAM-ServerCertificate
|
||||
@ -402,7 +479,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
|
||||
|
||||
@ -475,24 +552,6 @@ 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.
|
||||
@ -517,11 +576,31 @@ 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.
|
||||
|
||||
@ -535,7 +614,8 @@ that the `Authority` is associated with it Lemur allows that user to user/view/u
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
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 a 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,18 +6,19 @@ 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
|
||||
@ -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
|
||||
-----
|
||||
@ -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::
|
||||
|
@ -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
|
||||
|
||||
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@ -187,7 +180,7 @@ You'll use the builtin HttpProxyModule within Nginx to handle proxying
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
|
||||
location / {
|
||||
root /www/lemur/lemur/static/dist;
|
||||
include mime.types;
|
||||
@ -277,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,
|
||||
Get familiar with how Lemur works by reviewing the :doc:`../guide/index`. When you're ready
|
||||
see :doc:`../production/index` for more details on how to configure Lemur for production.
|
||||
|
||||
Remember the above just gets you going, but for production there are several different security considerations to take into account,
|
||||
remember Lemur is handling sensitive data and security is imperative.
|
||||
|
||||
See :doc:`../production/index` for more details on how to configure Lemur for production.
|
||||
|
@ -2,4 +2,28 @@ Jinja2>=2.3
|
||||
Pygments>=1.2
|
||||
Sphinx>=1.3
|
||||
docutils>=0.7
|
||||
markupsafe
|
||||
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
|
@ -48,8 +48,8 @@ gulp.task('test', function (done) {
|
||||
|
||||
gulp.task('dev:fonts', function () {
|
||||
var fileList = [
|
||||
'lemur/static/app/vendor/bower_components/bootstrap/dist/fonts/*',
|
||||
'lemur/static/app/vendor/bower_components/fontawesome/fonts/*'
|
||||
'bower_components/bootstrap/dist/fonts/*',
|
||||
'bower_components/fontawesome/fonts/*'
|
||||
];
|
||||
|
||||
return gulp.src(fileList)
|
||||
@ -57,7 +57,7 @@ gulp.task('dev:fonts', function () {
|
||||
});
|
||||
|
||||
gulp.task('dev:styles', function () {
|
||||
var baseContent = '@import "lemur/static/app/vendor/bower_components/bootstrap/less/bootstrap.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/variables.less";@import "lemur/static/app/vendor/bower_components/bootswatch/$theme$/bootswatch.less";@import "lemur/static/app/vendor/bower_components/bootstrap/less/utilities.less";';
|
||||
var baseContent = '@import "bower_components/bootstrap/less/bootstrap.less";@import "bower_components/bootswatch/$theme$/variables.less";@import "bower_components/bootswatch/$theme$/bootswatch.less";@import "bower_components/bootstrap/less/utilities.less";';
|
||||
var isBootswatchFile = function (file) {
|
||||
|
||||
var suffix = 'bootswatch.less';
|
||||
@ -73,15 +73,15 @@ gulp.task('dev:styles', function () {
|
||||
|
||||
var fileList = [
|
||||
'lemur/static/app/styles/lemur.css',
|
||||
'lemur/static/app/vendor/bower_components/bootswatch/sandstone/bootswatch.less',
|
||||
'lemur/static/app/vendor/bower_components/fontawesome/css/font-awesome.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-spinkit/src/angular-spinkit.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-chart.js/dist/angular-chart.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-loading-bar/src/loading-bar.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-ui-switch/angular-ui-switch.css',
|
||||
'lemur/static/app/vendor/bower_components/angular-wizard/dist/angular-wizard.css',
|
||||
'lemur/static/app/vendor/bower_components/ng-table/ng-table.css',
|
||||
'lemur/static/app/vendor/bower_components/angularjs-toaster/toaster.css'
|
||||
'bower_components/bootswatch/sandstone/bootswatch.less',
|
||||
'bower_components/fontawesome/css/font-awesome.css',
|
||||
'bower_components/angular-spinkit/src/angular-spinkit.css',
|
||||
'bower_components/angular-chart.js/dist/angular-chart.css',
|
||||
'bower_components/angular-loading-bar/src/loading-bar.css',
|
||||
'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'
|
||||
];
|
||||
|
||||
return gulp.src(fileList)
|
||||
|
@ -27,7 +27,10 @@ function browserSyncInit(baseDir, files, browser) {
|
||||
browserSync.instance = browserSync.init(files, {
|
||||
startPath: '/index.html',
|
||||
server: {
|
||||
baseDir: baseDir
|
||||
baseDir: baseDir,
|
||||
routes: {
|
||||
'/bower_components': './bower_components'
|
||||
}
|
||||
},
|
||||
browser: browser,
|
||||
ghostMode: false
|
||||
|
@ -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
|
||||
@ -54,7 +54,7 @@ def configure_hook(app):
|
||||
from lemur.decorators import crossdomain
|
||||
if app.config.get('CORS'):
|
||||
@app.after_request
|
||||
@crossdomain(origin="http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
|
||||
@crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE'])
|
||||
def after(response):
|
||||
return response
|
||||
|
||||
|
@ -16,24 +16,19 @@ operator_permission = Permission(RoleNeed('operator'))
|
||||
admin_permission = Permission(RoleNeed('admin'))
|
||||
|
||||
CertificateCreator = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'certificateView')
|
||||
|
||||
CertificateOwner = namedtuple('certificate', ['method', 'value'])
|
||||
CertificateOwnerNeed = partial(CertificateOwner, 'certificateView')
|
||||
CertificateCreatorNeed = partial(CertificateCreator, 'key')
|
||||
|
||||
|
||||
class ViewKeyPermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(str(certificate_id))
|
||||
o_need = CertificateOwnerNeed(str(role_id))
|
||||
super(ViewKeyPermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
super(ViewKeyPermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
|
||||
|
||||
class UpdateCertificatePermission(Permission):
|
||||
def __init__(self, role_id, certificate_id):
|
||||
c_need = CertificateCreatorNeed(str(certificate_id))
|
||||
o_need = CertificateOwnerNeed(str(role_id))
|
||||
super(UpdateCertificatePermission, self).__init__(o_need, c_need, RoleNeed('admin'))
|
||||
def __init__(self, certificate_id, owner):
|
||||
c_need = CertificateCreatorNeed(certificate_id)
|
||||
super(UpdateCertificatePermission, self).__init__(c_need, RoleNeed(owner), RoleNeed('admin'))
|
||||
|
||||
|
||||
RoleUser = namedtuple('role', ['method', 'value'])
|
||||
@ -42,7 +37,7 @@ ViewRoleCredentialsNeed = partial(RoleUser, 'roleView')
|
||||
|
||||
class ViewRoleCredentialsPermission(Permission):
|
||||
def __init__(self, role_id):
|
||||
need = ViewRoleCredentialsNeed(str(role_id))
|
||||
need = ViewRoleCredentialsNeed(role_id)
|
||||
super(ViewRoleCredentialsPermission, self).__init__(need, RoleNeed('admin'))
|
||||
|
||||
|
||||
|
@ -8,11 +8,12 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from builtins import bytes
|
||||
import jwt
|
||||
import json
|
||||
import base64
|
||||
import binascii
|
||||
from builtins import str
|
||||
|
||||
from functools import wraps
|
||||
from datetime import datetime, timedelta
|
||||
@ -29,24 +30,21 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
|
||||
|
||||
from lemur.users import service as user_service
|
||||
from lemur.auth.permissions import CertificateOwnerNeed, CertificateCreatorNeed, \
|
||||
from lemur.auth.permissions import CertificateCreatorNeed, \
|
||||
AuthorityCreatorNeed, ViewRoleCredentialsNeed
|
||||
|
||||
|
||||
def base64url_decode(data):
|
||||
if isinstance(data, str):
|
||||
data = str(data)
|
||||
|
||||
rem = len(data) % 4
|
||||
|
||||
if rem > 0:
|
||||
data += b'=' * (4 - rem)
|
||||
data += '=' * (4 - rem)
|
||||
|
||||
return base64.urlsafe_b64decode(data)
|
||||
return base64.urlsafe_b64decode(bytes(data.encode('latin-1')))
|
||||
|
||||
|
||||
def base64url_encode(data):
|
||||
return base64.urlsafe_b64encode(data).replace(b'=', b'')
|
||||
return base64.urlsafe_b64encode(data).replace('=', '')
|
||||
|
||||
|
||||
def get_rsa_public_key(n, e):
|
||||
@ -141,9 +139,11 @@ def fetch_token_header(token):
|
||||
|
||||
try:
|
||||
return json.loads(base64url_decode(header_segment))
|
||||
except TypeError:
|
||||
except TypeError as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
except binascii.Error:
|
||||
except binascii.Error as e:
|
||||
current_app.logger.exception(e)
|
||||
raise jwt.DecodeError('Invalid header padding')
|
||||
|
||||
|
||||
@ -165,7 +165,6 @@ def on_identity_loaded(sender, identity):
|
||||
# identity with the roles that the user provides
|
||||
if hasattr(user, 'roles'):
|
||||
for role in user.roles:
|
||||
identity.provides.add(CertificateOwnerNeed(role.id))
|
||||
identity.provides.add(ViewRoleCredentialsNeed(role.id))
|
||||
identity.provides.add(RoleNeed(role.name))
|
||||
|
||||
|
@ -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)}
|
||||
|
||||
@ -183,10 +183,6 @@ class Ping(Resource):
|
||||
# update their google 'roles'
|
||||
roles = []
|
||||
|
||||
# Legacy edge case - 'admin' has some special privileges associated with it
|
||||
if 'secops@netflix.com' in profile['googleGroups']:
|
||||
roles.append(role_service.get_by_name('admin'))
|
||||
|
||||
for group in profile['googleGroups']:
|
||||
role = role_service.get_by_name(group)
|
||||
if not role:
|
||||
@ -196,10 +192,12 @@ class Ping(Resource):
|
||||
# if we get an sso user create them an account
|
||||
# we still pick a random password in case sso is down
|
||||
if not user:
|
||||
# every user is an operator (tied to the verisignCA)
|
||||
v = role_service.get_by_name('verisign')
|
||||
if v:
|
||||
roles.append(v)
|
||||
|
||||
# every user is an operator (tied to a default role)
|
||||
if current_app.config.get('LEMUR_DEFAULT_ROLE'):
|
||||
v = role_service.get_by_name(current_app.config.get('LEMUR_DEFAULT_ROLE'))
|
||||
if v:
|
||||
roles.append(v)
|
||||
|
||||
user = user_service.create(
|
||||
profile['email'],
|
||||
@ -222,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
|
||||
)
|
||||
|
||||
|
@ -22,7 +22,7 @@ from lemur.certificates.models import Certificate
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
def update(authority_id, active=None, roles=None):
|
||||
def update(authority_id, description=None, owner=None, active=None, roles=None):
|
||||
"""
|
||||
Update a an authority with new values.
|
||||
|
||||
@ -37,6 +37,9 @@ def update(authority_id, active=None, roles=None):
|
||||
|
||||
if active:
|
||||
authority.active = active
|
||||
|
||||
authority.description = description
|
||||
authority.owner = owner
|
||||
return database.update(authority)
|
||||
|
||||
|
||||
|
@ -20,6 +20,7 @@ from lemur.common.utils import paginated_parser, marshal_items
|
||||
|
||||
FIELDS = {
|
||||
'name': fields.String,
|
||||
'owner': fields.String,
|
||||
'description': fields.String,
|
||||
'options': fields.Raw,
|
||||
'pluginName': fields.String,
|
||||
@ -264,7 +265,9 @@ class Authorities(AuthenticatedResource):
|
||||
|
||||
{
|
||||
"roles": [],
|
||||
"active": false
|
||||
"active": false,
|
||||
"owner": "bob@example.com",
|
||||
"description": "this is authority1"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -279,12 +282,12 @@ class Authorities(AuthenticatedResource):
|
||||
"id": 1,
|
||||
"name": "authority1",
|
||||
"description": "this is authority1",
|
||||
"pluginname": null,
|
||||
"pluginName": null,
|
||||
"chain": "-----begin ...",
|
||||
"body": "-----begin ...",
|
||||
"active": false,
|
||||
"notbefore": "2015-06-05t17:09:39",
|
||||
"notafter": "2015-06-10t17:09:39"
|
||||
"notBefore": "2015-06-05t17:09:39",
|
||||
"notAfter": "2015-06-10t17:09:39"
|
||||
"options": null
|
||||
}
|
||||
|
||||
@ -292,8 +295,10 @@ class Authorities(AuthenticatedResource):
|
||||
:statuscode 200: no error
|
||||
:statuscode 403: unauthenticated
|
||||
"""
|
||||
self.reqparse.add_argument('roles', type=list, location='json')
|
||||
self.reqparse.add_argument('active', type=str, location='json')
|
||||
self.reqparse.add_argument('roles', type=list, default=[], location='json')
|
||||
self.reqparse.add_argument('active', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('owner', type=str, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json', required=True)
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
authority = service.get(authority_id)
|
||||
@ -315,7 +320,13 @@ class Authorities(AuthenticatedResource):
|
||||
return dict(message="You are not allowed to associate a role which you are not a member of"), 400
|
||||
|
||||
if permission.can():
|
||||
return service.update(authority_id, active=args['active'], roles=args['roles'])
|
||||
return service.update(
|
||||
authority_id,
|
||||
owner=args['owner'],
|
||||
description=args['description'],
|
||||
active=args['active'],
|
||||
roles=args['roles']
|
||||
)
|
||||
|
||||
return dict(message="You are not authorized to update this authority"), 403
|
||||
|
||||
|
@ -85,14 +85,28 @@ def update(cert_id, owner, description, active, destinations, notifications):
|
||||
:param active:
|
||||
:return:
|
||||
"""
|
||||
from lemur.notifications import service as notification_service
|
||||
cert = get(cert_id)
|
||||
cert.owner = owner
|
||||
cert.active = active
|
||||
cert.description = description
|
||||
|
||||
database.update_list(cert, 'notifications', Notification, notifications)
|
||||
# we might have to create new notifications if the owner changes
|
||||
new_notifications = []
|
||||
# get existing names to remove
|
||||
notification_name = "DEFAULT_{0}".format(cert.owner.split('@')[0].upper())
|
||||
for n in notifications:
|
||||
if notification_name not in n.label:
|
||||
new_notifications.append(n)
|
||||
|
||||
notification_name = "DEFAULT_{0}".format(owner.split('@')[0].upper())
|
||||
new_notifications += notification_service.create_default_expiration_notifications(notification_name, owner)
|
||||
|
||||
cert.notifications = new_notifications
|
||||
|
||||
database.update_list(cert, 'destinations', Destination, destinations)
|
||||
|
||||
cert.owner = owner
|
||||
|
||||
return database.update(cert)
|
||||
|
||||
|
||||
@ -168,6 +182,10 @@ def upload(**kwargs):
|
||||
kwargs.get('intermediate_cert'),
|
||||
)
|
||||
|
||||
# we override the generated name if one is provided
|
||||
if kwargs.get('name'):
|
||||
cert.name = kwargs['name']
|
||||
|
||||
cert.description = kwargs.get('description')
|
||||
|
||||
cert.owner = kwargs['owner']
|
||||
@ -214,13 +232,14 @@ 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])
|
||||
|
||||
notification_name = 'DEFAULT_SECURITY'
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
notifications += notification_service.create_default_expiration_notifications(notification_name,
|
||||
current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL'))
|
||||
cert.notifications = notifications
|
||||
|
||||
database.update(cert)
|
||||
@ -319,17 +338,18 @@ def create_csr(csr_config):
|
||||
x509.BasicConstraints(ca=False, path_length=None), critical=True,
|
||||
)
|
||||
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'subAltNames':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
if csr_config.get('extensions'):
|
||||
for k, v in csr_config.get('extensions', {}).items():
|
||||
if k == 'subAltNames':
|
||||
# map types to their x509 objects
|
||||
general_names = []
|
||||
for name in v['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
general_names.append(x509.DNSName(name['value']))
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName(general_names), critical=True
|
||||
)
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName(general_names), critical=True
|
||||
)
|
||||
|
||||
# TODO support more CSR options, none of the authority plugins currently support these options
|
||||
# builder.add_extension(
|
||||
|
@ -6,14 +6,28 @@
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import requests
|
||||
import subprocess
|
||||
from OpenSSL import crypto
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from contextlib import contextmanager
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mktempfile():
|
||||
with NamedTemporaryFile(delete=False) as f:
|
||||
name = f.name
|
||||
|
||||
try:
|
||||
yield name
|
||||
finally:
|
||||
os.unlink(name)
|
||||
|
||||
|
||||
def ocsp_verify(cert_path, issuer_chain_path):
|
||||
"""
|
||||
@ -53,27 +67,18 @@ def crl_verify(cert_path):
|
||||
:return: True if certificate is valid, False otherwise
|
||||
:raise Exception: If certificate does not have CRL
|
||||
"""
|
||||
s = "(http(s)?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}/\S*?$)"
|
||||
regex = re.compile(s, re.MULTILINE)
|
||||
with open(cert_path, 'rt') as c:
|
||||
cert = x509.load_pem_x509_certificate(c.read(), default_backend())
|
||||
|
||||
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_path, 'rt').read())
|
||||
for x in range(x509.get_extension_count()):
|
||||
ext = x509.get_extension(x)
|
||||
if ext.get_short_name() == 'crlDistributionPoints':
|
||||
r = regex.search(ext.get_data())
|
||||
points = r.groups()
|
||||
break
|
||||
else:
|
||||
raise Exception("Certificate does not have a CRL distribution point")
|
||||
|
||||
for point in points:
|
||||
if point:
|
||||
response = requests.get(point)
|
||||
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content)
|
||||
revoked = crl.get_revoked()
|
||||
for r in revoked:
|
||||
if x509.get_serial_number() == r.get_serial():
|
||||
return
|
||||
distribution_points = cert.extensions.get_extension_for_oid(x509.OID_CRL_DISTRIBUTION_POINTS).value
|
||||
for p in distribution_points:
|
||||
point = p.full_name[0].value
|
||||
response = requests.get(point)
|
||||
crl = crypto.load_crl(crypto.FILETYPE_ASN1, response.content) # TODO this should be switched to cryptography when support exists
|
||||
revoked = crl.get_revoked()
|
||||
for r in revoked:
|
||||
if cert.serial == r.get_serial():
|
||||
return
|
||||
return True
|
||||
|
||||
|
||||
@ -99,22 +104,6 @@ def verify(cert_path, issuer_chain_path):
|
||||
raise Exception("Failed to verify")
|
||||
|
||||
|
||||
def make_tmp_file(string):
|
||||
"""
|
||||
Creates a temporary file for a given string
|
||||
|
||||
:param string:
|
||||
:return: Full file path to created file
|
||||
"""
|
||||
m = hashlib.md5()
|
||||
m.update(string)
|
||||
hexdigest = m.hexdigest()
|
||||
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), hexdigest)
|
||||
with open(path, 'w') as f:
|
||||
f.write(string)
|
||||
return path
|
||||
|
||||
|
||||
def verify_string(cert_string, issuer_string):
|
||||
"""
|
||||
Verify a certificate given only it's string value
|
||||
@ -123,13 +112,11 @@ def verify_string(cert_string, issuer_string):
|
||||
:param issuer_string:
|
||||
:return: True if valid, False otherwise
|
||||
"""
|
||||
cert_path = make_tmp_file(cert_string)
|
||||
issuer_path = make_tmp_file(issuer_string)
|
||||
status = verify(cert_path, issuer_path)
|
||||
remove_tmp_file(cert_path)
|
||||
remove_tmp_file(issuer_path)
|
||||
with mktempfile() as cert_tmp:
|
||||
with open(cert_tmp, 'w') as f:
|
||||
f.write(cert_string)
|
||||
with mktempfile() as issuer_tmp:
|
||||
with open(issuer_tmp, 'w') as f:
|
||||
f.write(issuer_string)
|
||||
status = verify(cert_tmp, issuer_tmp)
|
||||
return status
|
||||
|
||||
|
||||
def remove_tmp_file(file_path):
|
||||
os.remove(file_path)
|
||||
|
@ -24,6 +24,8 @@ from lemur.roles import service as role_service
|
||||
|
||||
from lemur.common.utils import marshal_items, paginated_parser
|
||||
|
||||
from lemur.notifications.views import notification_list
|
||||
|
||||
|
||||
mod = Blueprint('certificates', __name__)
|
||||
api = Api(mod)
|
||||
@ -206,6 +208,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": {
|
||||
@ -274,18 +316,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('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('authority', type=valid_authority, location='json', required=True)
|
||||
self.reqparse.add_argument('description', type=str, location='json', required=True)
|
||||
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()
|
||||
|
||||
@ -332,7 +373,8 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
"intermediateCert": "---Begin Public...",
|
||||
"privateKey": "---Begin Private..."
|
||||
"destinations": [],
|
||||
"notifications": []
|
||||
"notifications": [],
|
||||
"name": "cert1"
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@ -373,6 +415,7 @@ class CertificatesUpload(AuthenticatedResource):
|
||||
"""
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('owner', type=str, required=True, location='json')
|
||||
self.reqparse.add_argument('name', type=str, location='json')
|
||||
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
|
||||
self.reqparse.add_argument('destinations', type=list, default=[], dest='destinations', location='json')
|
||||
self.reqparse.add_argument('notifications', type=list, default=[], dest='notifications', location='json')
|
||||
@ -446,7 +489,7 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
|
||||
permission = ViewKeyPermission(certificate_id, hasattr(role, 'id'))
|
||||
permission = ViewKeyPermission(certificate_id, getattr(role, 'name', None))
|
||||
|
||||
if permission.can():
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
@ -567,12 +610,13 @@ class Certificates(AuthenticatedResource):
|
||||
self.reqparse.add_argument('owner', type=str, location='json')
|
||||
self.reqparse.add_argument('description', type=str, 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('notifications', type=notification_list, default=[], location='json')
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
cert = service.get(certificate_id)
|
||||
role = role_service.get_by_name(cert.owner)
|
||||
permission = UpdateCertificatePermission(certificate_id, hasattr(role, 'id'))
|
||||
|
||||
permission = UpdateCertificatePermission(certificate_id, getattr(role, 'name', None))
|
||||
|
||||
if permission.can():
|
||||
return service.update(
|
||||
@ -662,6 +706,7 @@ class NotificationCertificatesList(AuthenticatedResource):
|
||||
args['notification_id'] = notification_id
|
||||
return service.render(args)
|
||||
|
||||
|
||||
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')
|
||||
|
@ -9,10 +9,9 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy.sql import and_, or_
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
|
||||
from lemur.extensions import db
|
||||
from lemur.exceptions import AttrNotFound, DuplicateError
|
||||
@ -126,8 +125,7 @@ def get(model, value, field="id"):
|
||||
query = session_query(model)
|
||||
try:
|
||||
return query.filter(getattr(model, field) == value).one()
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
except NoResultFound as e:
|
||||
return
|
||||
|
||||
|
||||
|
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')
|
306
lemur/manage.py
@ -1,7 +1,11 @@
|
||||
from __future__ import unicode_literals # at top of module
|
||||
|
||||
import os
|
||||
import sys
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
from gunicorn.config import make_settings
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
@ -77,7 +81,15 @@ LEMUR_RESTRICTED_DOMAINS = []
|
||||
|
||||
LEMUR_EMAIL = ''
|
||||
LEMUR_SECURITY_TEAM_EMAIL = []
|
||||
LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS = [30, 15, 2]
|
||||
|
||||
# Certificate Defaults
|
||||
|
||||
LEMUR_DEFAULT_COUNTRY = ''
|
||||
LEMUR_DEFAULT_STATE = ''
|
||||
LEMUR_DEFAULT_LOCATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATION = ''
|
||||
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
|
||||
|
||||
|
||||
# Logging
|
||||
|
||||
@ -100,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 = ''
|
||||
@ -136,12 +141,15 @@ def check_revoked():
|
||||
as `unknown`.
|
||||
"""
|
||||
for cert in cert_service.get_all_certs():
|
||||
if cert.chain:
|
||||
status = verify_string(cert.body, cert.chain)
|
||||
else:
|
||||
status = verify_string(cert.body, "")
|
||||
try:
|
||||
if cert.chain:
|
||||
status = verify_string(cert.body, cert.chain)
|
||||
else:
|
||||
status = verify_string(cert.body, "")
|
||||
|
||||
cert.status = 'valid' if status else "invalid"
|
||||
cert.status = 'valid' if status else 'invalid'
|
||||
except Exception as e:
|
||||
cert.status = 'unknown'
|
||||
database.update(cert)
|
||||
|
||||
|
||||
@ -171,19 +179,18 @@ def generate_settings():
|
||||
return output
|
||||
|
||||
|
||||
@manager.option('-s', '--sources', dest='labels', default='', required=False)
|
||||
@manager.option('-l', '--list', dest='view', default=False, required=False)
|
||||
def sync_sources(labels, view):
|
||||
@manager.option('-s', '--sources', dest='labels')
|
||||
def sync_sources(labels):
|
||||
"""
|
||||
Attempts to run several methods Certificate discovery. This is
|
||||
run on a periodic basis and updates the Lemur datastore with the
|
||||
information it discovers.
|
||||
"""
|
||||
if view:
|
||||
if not labels:
|
||||
sys.stdout.write("Active\tLabel\tDescription\n")
|
||||
for source in source_service.get_all():
|
||||
sys.stdout.write(
|
||||
"[{active}]\t{label}\t{description}!\n".format(
|
||||
"{active}\t{label}\t{description}!\n".format(
|
||||
label=source.label,
|
||||
description=source.description,
|
||||
active=source.active
|
||||
@ -198,13 +205,14 @@ def sync_sources(labels, view):
|
||||
try:
|
||||
sync_lock.acquire(timeout=10) # wait up to 10 seconds
|
||||
|
||||
if labels:
|
||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||
labels = labels.split(",")
|
||||
else:
|
||||
sys.stdout.write("[+] Starting to sync ALL sources!\n")
|
||||
sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels))
|
||||
labels = labels.split(",")
|
||||
|
||||
if labels[0] == 'all':
|
||||
sync()
|
||||
else:
|
||||
sync(labels=labels)
|
||||
|
||||
sync(labels=labels)
|
||||
sys.stdout.write(
|
||||
"[+] Finished syncing sources. Run Time: {time}\n".format(
|
||||
time=(time.time() - start_time)
|
||||
@ -248,18 +256,23 @@ 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:
|
||||
sys.stdout.write("We need to set Lemur's password to continue!\n")
|
||||
password1 = prompt_pass("Password")
|
||||
password2 = prompt_pass("Confirm Password")
|
||||
if not password:
|
||||
sys.stdout.write("We need to set Lemur's password to continue!\n")
|
||||
password = prompt_pass("Password")
|
||||
password1 = prompt_pass("Confirm Password")
|
||||
|
||||
if password1 != password2:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
if password != password1:
|
||||
sys.stderr.write("[!] Passwords do not match!\n")
|
||||
sys.exit(1)
|
||||
|
||||
role = role_service.get_by_name('admin')
|
||||
|
||||
@ -270,16 +283,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),
|
||||
@ -480,6 +493,229 @@ def unlock(path=None):
|
||||
sys.stdout.write("[+] Keys have been unencrypted!\n")
|
||||
|
||||
|
||||
def unicode_(data):
|
||||
import sys
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
return data.decode('UTF-8')
|
||||
return data
|
||||
|
||||
|
||||
class ProvisionELB(Command):
|
||||
"""
|
||||
Creates and provisions a certificate on an ELB based on command line arguments
|
||||
"""
|
||||
|
||||
option_list = (
|
||||
Option('-d', '--dns', dest='dns', action='append', required=True, type=unicode_),
|
||||
Option('-e', '--elb', dest='elb_name', required=True, type=unicode_),
|
||||
Option('-o', '--owner', dest='owner', type=unicode_),
|
||||
Option('-a', '--authority', dest='authority', required=True, type=unicode_),
|
||||
Option('-s', '--description', dest='description', default=u'Command line provisioned keypair', type=unicode_),
|
||||
Option('-t', '--destination', dest='destinations', action='append', type=unicode_, required=True),
|
||||
Option('-n', '--notification', dest='notifications', action='append', type=unicode_, default=[]),
|
||||
Option('-r', '--region', dest='region', default=u'us-east-1', type=unicode_),
|
||||
Option('-p', '--dport', '--port', dest='dport', default=7002),
|
||||
Option('--src-port', '--source-port', '--sport', dest='sport', default=443),
|
||||
Option('--dry-run', dest='dryrun', action='store_true')
|
||||
)
|
||||
|
||||
def configure_user(self, owner):
|
||||
from flask import g
|
||||
import lemur.users.service
|
||||
|
||||
# grab the user
|
||||
g.user = lemur.users.service.get_by_username(owner)
|
||||
# get the first user by default
|
||||
if not g.user:
|
||||
g.user = lemur.users.service.get_all()[0]
|
||||
|
||||
return g.user.username
|
||||
|
||||
def build_cert_options(self, destinations, notifications, description, owner, dns, authority):
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from lemur.certificates.views import valid_authority
|
||||
import sys
|
||||
|
||||
# convert argument lists to arrays, or empty sets
|
||||
destinations = self.get_destinations(destinations)
|
||||
if not destinations:
|
||||
sys.stderr.write("Valid destinations provided\n")
|
||||
sys.exit(1)
|
||||
|
||||
# get the primary CN
|
||||
common_name = dns[0]
|
||||
|
||||
# If there are more than one fqdn, add them as alternate names
|
||||
extensions = {}
|
||||
if len(dns) > 1:
|
||||
extensions['subAltNames'] = {'names': map(lambda x: {'nameType': 'DNSName', 'value': x}, dns)}
|
||||
|
||||
try:
|
||||
authority = valid_authority({"name": authority})
|
||||
except NoResultFound:
|
||||
sys.stderr.write("Invalid authority specified: '{}'\naborting\n".format(authority))
|
||||
sys.exit(1)
|
||||
|
||||
options = {
|
||||
# Convert from the Destination model to the JSON input expected further in the code
|
||||
'destinations': map(lambda x: {'id': x.id, 'label': x.label}, destinations),
|
||||
'description': description,
|
||||
'notifications': notifications,
|
||||
'commonName': common_name,
|
||||
'extensions': extensions,
|
||||
'authority': authority,
|
||||
'owner': owner,
|
||||
# defaults:
|
||||
'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
|
||||
|
||||
def get_destinations(self, destination_names):
|
||||
from lemur.destinations import service
|
||||
|
||||
destinations = []
|
||||
|
||||
for destination_name in destination_names:
|
||||
destination = service.get_by_label(destination_name)
|
||||
|
||||
if not destination:
|
||||
sys.stderr.write("Invalid destination specified: '{}'\nAborting...\n".format(destination_name))
|
||||
sys.exit(1)
|
||||
|
||||
destinations.append(service.get_by_label(destination_name))
|
||||
|
||||
return destinations
|
||||
|
||||
def check_duplicate_listener(self, elb_name, region, account, sport, dport):
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
|
||||
listeners = elb.get_listeners(account, region, elb_name)
|
||||
for listener in listeners:
|
||||
if listener[0] == sport and listener[1] == dport:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_destination_account(self, destinations):
|
||||
for destination in self.get_destinations(destinations):
|
||||
if destination.plugin_name == 'aws-destination':
|
||||
|
||||
account_number = destination.plugin.get_option('accountNumber', destination.options)
|
||||
return account_number
|
||||
|
||||
sys.stderr.write("No destination AWS account provided, failing\n")
|
||||
sys.exit(1)
|
||||
|
||||
def run(self, dns, elb_name, owner, authority, description, notifications, destinations, region, dport, sport,
|
||||
dryrun):
|
||||
from lemur.certificates import service
|
||||
from lemur.plugins.lemur_aws import elb
|
||||
from boto.exception import BotoServerError
|
||||
|
||||
# configure the owner if we can find it, or go for default, and put it in the global
|
||||
owner = self.configure_user(owner)
|
||||
|
||||
# make a config blob from the command line arguments
|
||||
cert_options = self.build_cert_options(
|
||||
destinations=destinations,
|
||||
notifications=notifications,
|
||||
description=description,
|
||||
owner=owner,
|
||||
dns=dns,
|
||||
authority=authority)
|
||||
|
||||
aws_account = self.get_destination_account(destinations)
|
||||
|
||||
if dryrun:
|
||||
import json
|
||||
|
||||
cert_options['authority'] = cert_options['authority'].name
|
||||
sys.stdout.write('Will create certificate using options: {}\n'
|
||||
.format(json.dumps(cert_options, sort_keys=True, indent=2)))
|
||||
sys.stdout.write('Will create listener {}->{} HTTPS using the new certificate to elb {}\n'
|
||||
.format(sport, dport, elb_name))
|
||||
sys.exit(0)
|
||||
|
||||
if self.check_duplicate_listener(elb_name, region, aws_account, sport, dport):
|
||||
sys.stderr.write("ELB {} already has a listener {}->{}\nAborting...\n".format(elb_name, sport, dport))
|
||||
sys.exit(1)
|
||||
|
||||
# create the certificate
|
||||
try:
|
||||
sys.stdout.write('Creating certificate for {}\n'.format(cert_options['commonName']))
|
||||
cert = service.create(**cert_options)
|
||||
except Exception as e:
|
||||
if e.message == 'Duplicate certificate: a certificate with the same common name exists already':
|
||||
sys.stderr.write("Certificate already exists named: {}\n".format(dns[0]))
|
||||
sys.exit(1)
|
||||
raise e
|
||||
|
||||
cert_arn = cert.get_arn(aws_account)
|
||||
sys.stderr.write('cert arn: {}\n'.format(cert_arn))
|
||||
|
||||
sys.stderr.write('Configuring elb {} from port {} to port {} in region {} with cert {}\n'
|
||||
.format(elb_name, sport, dport, region, cert_arn))
|
||||
|
||||
delay = 1
|
||||
done = False
|
||||
retries = 5
|
||||
while not done and retries > 0:
|
||||
try:
|
||||
elb.create_new_listeners(aws_account, region, elb_name, [(sport, dport, 'HTTPS', cert_arn)])
|
||||
except BotoServerError as bse:
|
||||
# if the server returns ad error, the certificate
|
||||
if bse.error_code == 'CertificateNotFound':
|
||||
sys.stderr.write('Certificate not available yet in the AWS account, waiting {}, {} retries left\n'
|
||||
.format(delay, retries))
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
retries -= 1
|
||||
elif bse.error_code == 'DuplicateListener':
|
||||
sys.stderr.write('ELB {} already has a listener {}->{}'.format(elb_name, sport, dport))
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise bse
|
||||
else:
|
||||
done = True
|
||||
|
||||
|
||||
@manager.command
|
||||
def publish_verisign_units():
|
||||
"""
|
||||
Simple function that queries verisign for API units and posts the mertics to
|
||||
Atlas API for other teams to consume.
|
||||
:return:
|
||||
"""
|
||||
from lemur.plugins import plugins
|
||||
v = plugins.get('verisign-issuer')
|
||||
units = v.get_available_units()
|
||||
|
||||
metrics = {}
|
||||
for item in units:
|
||||
if item['@type'] in metrics.keys():
|
||||
metrics[item['@type']] += int(item['@remaining'])
|
||||
else:
|
||||
metrics.update({item['@type']: int(item['@remaining'])})
|
||||
|
||||
for name, value in metrics.items():
|
||||
metric = [
|
||||
{
|
||||
"timestamp": 1321351651,
|
||||
"type": "GAUGE",
|
||||
"name": "Symantec {0} Unit Count".format(name),
|
||||
"tags": {},
|
||||
"value": value
|
||||
}
|
||||
]
|
||||
|
||||
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
||||
|
||||
|
||||
def main():
|
||||
manager.add_command("start", LemurServer())
|
||||
manager.add_command("runserver", Server(host='127.0.0.1'))
|
||||
@ -489,8 +725,8 @@ def main():
|
||||
manager.add_command("init", InitializeApp())
|
||||
manager.add_command("create_user", CreateUser())
|
||||
manager.add_command("create_role", CreateRole())
|
||||
manager.add_command("provision_elb", ProvisionELB())
|
||||
manager.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -9,7 +9,6 @@
|
||||
|
||||
"""
|
||||
import ssl
|
||||
import socket
|
||||
|
||||
import arrow
|
||||
|
||||
@ -38,7 +37,10 @@ def _get_message_data(cert):
|
||||
:return:
|
||||
"""
|
||||
cert_dict = cert.as_dict()
|
||||
cert_dict['creator'] = cert.user.email
|
||||
|
||||
if cert.user:
|
||||
cert_dict['creator'] = cert.user.email
|
||||
|
||||
cert_dict['domains'] = [x .name for x in cert.domains]
|
||||
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert) if cert.name != x]))
|
||||
return cert_dict
|
||||
@ -56,13 +58,18 @@ def _deduplicate(messages):
|
||||
|
||||
for m, r, o in roll_ups:
|
||||
if r == targets:
|
||||
m.append(data)
|
||||
current_app.logger.info(
|
||||
"Sending expiration alert about {0} to {1}".format(
|
||||
data['name'], ",".join(targets)))
|
||||
for cert in m:
|
||||
if cert['body'] == data['body']:
|
||||
break
|
||||
else:
|
||||
m.append(data)
|
||||
current_app.logger.info(
|
||||
"Sending expiration alert about {0} to {1}".format(
|
||||
data['name'], ",".join(targets)))
|
||||
break
|
||||
else:
|
||||
roll_ups.append(([data], targets, options))
|
||||
|
||||
return roll_ups
|
||||
|
||||
|
||||
@ -106,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):
|
||||
@ -178,6 +186,9 @@ def create_default_expiration_notifications(name, recipients):
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
if not recipients:
|
||||
return []
|
||||
|
||||
options = [
|
||||
{
|
||||
'name': 'unit',
|
||||
@ -198,7 +209,7 @@ def create_default_expiration_notifications(name, recipients):
|
||||
},
|
||||
]
|
||||
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS")
|
||||
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [30, 15, 2])
|
||||
|
||||
notifications = []
|
||||
for i in intervals:
|
||||
|
@ -28,6 +28,36 @@ FIELDS = {
|
||||
}
|
||||
|
||||
|
||||
def notification(value, name):
|
||||
"""
|
||||
Validates a given notification exits
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
n = service.get(value)
|
||||
if not n:
|
||||
raise ValueError("Unable to find notification specified")
|
||||
return n
|
||||
|
||||
|
||||
def notification_list(value, name):
|
||||
"""
|
||||
Validates a given notification exists and returns a list
|
||||
:param value:
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
notifications = []
|
||||
for v in value:
|
||||
try:
|
||||
notifications.append(notification(v['id'], 'id'))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return notifications
|
||||
|
||||
|
||||
class NotificationsList(AuthenticatedResource):
|
||||
""" Defines the 'notifications' endpoint """
|
||||
def __init__(self):
|
||||
|
@ -110,7 +110,7 @@ class IPlugin(local):
|
||||
|
||||
def get_option(self, name, options):
|
||||
for o in options:
|
||||
if o.get(name):
|
||||
if o.get('name') == name:
|
||||
return o['value']
|
||||
|
||||
|
||||
|
@ -138,3 +138,19 @@ def delete_listeners(account_number, region, name, ports):
|
||||
:return:
|
||||
"""
|
||||
return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports)
|
||||
|
||||
|
||||
def get_listeners(account_number, region, name):
|
||||
"""
|
||||
Gets the listeners configured on an elb and returns a array of tuples
|
||||
|
||||
:param account_number:
|
||||
:param region:
|
||||
:param name:
|
||||
:return: list of tuples
|
||||
"""
|
||||
|
||||
conn = assume_service(account_number, 'elb', region)
|
||||
elbs = conn.get_all_load_balancers(load_balancer_names=[name])
|
||||
if elbs:
|
||||
return elbs[0].listeners
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
from boto.exception import BotoServerError
|
||||
from lemur.plugins.bases import DestinationPlugin, SourcePlugin
|
||||
from lemur.plugins.lemur_aws import iam, elb
|
||||
from lemur.plugins import lemur_aws as aws
|
||||
@ -42,7 +43,11 @@ class AWSDestinationPlugin(DestinationPlugin):
|
||||
# }
|
||||
|
||||
def upload(self, name, body, private_key, cert_chain, options, **kwargs):
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
try:
|
||||
iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain)
|
||||
except BotoServerError as e:
|
||||
if e.error_code != 'EntityAlreadyExists':
|
||||
raise Exception(e)
|
||||
|
||||
e = find_value('elb', options)
|
||||
if e:
|
||||
|
@ -1,5 +0,0 @@
|
||||
try:
|
||||
VERSION = __import__('pkg_resources') \
|
||||
.get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = 'unknown'
|
@ -1,352 +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 ssl
|
||||
import base64
|
||||
from json import dumps
|
||||
|
||||
import arrow
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
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
|
||||
|
||||
API_ENDPOINT = '/v1/ca/netflix' # TODO this should be configurable
|
||||
|
||||
|
||||
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
|
||||
response = self.session.post(self.url + endpoint, data=data, timeout=10, verify=self.ca_bundle)
|
||||
return process_response(response)
|
||||
|
||||
def get(self, endpoint):
|
||||
"""
|
||||
HTTP GET to CloudCA
|
||||
|
||||
:param endpoint:
|
||||
:return:
|
||||
"""
|
||||
response = self.session.get(self.url + endpoint, timeout=10, verify=self.ca_bundle)
|
||||
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(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(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()
|
||||
|
||||
response = self.session.post(self.url + endpoint, data=dumps(remove_none(options)), timeout=10,
|
||||
verify=self.ca_bundle)
|
||||
|
||||
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(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': 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, **kwargs):
|
||||
certs = []
|
||||
for authority in self.get_authorities():
|
||||
certs += self.get_cert(ca_name=authority)
|
||||
return
|
||||
|
||||
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(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_cert': "\n".join(intermediates),
|
||||
'owner': c['ownerEmail']
|
||||
})
|
||||
|
||||
return certs
|
@ -53,9 +53,9 @@ class EmailNotificationPlugin(ExpirationNotificationPlugin):
|
||||
|
||||
# jinja template depending on type
|
||||
template = env.get_template('{}.html'.format(event_type))
|
||||
body = template.render(**kwargs)
|
||||
body = template.render(dict(messages=message, hostname=current_app.config.get('LEMUR_HOSTNAME')))
|
||||
|
||||
s_type = current_app.config.get("LEMUR_EMAIL_SENDER").lower()
|
||||
s_type = current_app.config.get("LEMUR_EMAIL_SENDER", 'ses').lower()
|
||||
if s_type == 'ses':
|
||||
conn = boto.connect_ses()
|
||||
conn.send_email(current_app.config.get("LEMUR_EMAIL"), subject, body, targets, format='html')
|
||||
|
@ -1,4 +1,5 @@
|
||||
from jinja2 import Environment, PackageLoader
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
loader = PackageLoader('lemur')
|
||||
loader = FileSystemLoader(searchpath=os.path.dirname(os.path.realpath(__file__)))
|
||||
env = Environment(loader=loader)
|
||||
|
@ -49,11 +49,16 @@
|
||||
<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>
|
||||
Lemur, Netflix's SSL management portal 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. Visit https://lemur.netflix.com/#/certificates/create to reissue them.
|
||||
<p>
|
||||
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>
|
||||
Visit https://{{ hostname }}/#/certificates/create to reissue them.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% for message in messages %}
|
||||
@ -78,6 +83,12 @@
|
||||
<tr>
|
||||
<td>{{ message.creator }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Description</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ message.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Not Before</strong></td>
|
||||
</tr>
|
||||
@ -104,20 +115,6 @@
|
||||
<td>Unknown</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><strong>Associated ELBs</strong></td>
|
||||
</tr>
|
||||
{% if message.listeners %}
|
||||
{% for name in message.listeners %}
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>None</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><strong>Potentially Superseded by</strong> (Lemur's best guess)</td>
|
||||
</tr>
|
||||
@ -139,7 +136,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-top: 0px" align="center" valign="top">
|
||||
<em style="font-style:italic; font-size: 12px; color: #aaa;">Lemur is broken regularly by <a style="color: #29abe0; text-decoration: none;" href="mailto:secops@netflix.com">Security Operations</a></em>
|
||||
<em style="font-style:italic; font-size: 12px; color: #aaa;">Lemur is broken regularly by Netflix</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -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
|
||||
|
||||
|
||||
@ -56,6 +55,7 @@ VERISIGN_ERRORS = {
|
||||
"0x4828": "Verisign certificates can be at most two years in length",
|
||||
"0x3043": "Certificates must have a validity of at least 1 day",
|
||||
"0x950b": "CSR: Invalid State",
|
||||
"0x3105": "Organization Name Not Matched",
|
||||
}
|
||||
|
||||
|
||||
@ -131,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()
|
||||
@ -146,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
|
||||
@ -155,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):
|
||||
@ -167,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):
|
||||
"""
|
||||
@ -176,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()
|
@ -140,7 +140,7 @@ class RolesList(AuthenticatedResource):
|
||||
self.reqparse.add_argument('description', type=str, location='json')
|
||||
self.reqparse.add_argument('username', type=str, location='json')
|
||||
self.reqparse.add_argument('password', type=str, location='json')
|
||||
self.reqparse.add_argument('users', type=dict, location='json')
|
||||
self.reqparse.add_argument('users', type=list, location='json')
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
return service.create(args['name'], args.get('password'), args.get('description'), args.get('username'),
|
||||
|
@ -39,6 +39,7 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so
|
||||
|
||||
def sync_create(certificate, source):
|
||||
cert = cert_service.import_certificate(**certificate)
|
||||
cert.description = "This certificate was automatically discovered by Lemur"
|
||||
cert.sources.append(source)
|
||||
sync_update_destination(cert, source)
|
||||
database.update(cert)
|
||||
|
11
lemur/static/app/angular/app.js
vendored
@ -25,7 +25,7 @@ var lemur = angular
|
||||
});
|
||||
|
||||
$authProvider.oauth2({
|
||||
name: 'ping',
|
||||
name: 'example',
|
||||
url: 'http://localhost:5000/api/1/auth/ping',
|
||||
redirectUri: 'http://localhost:3000/',
|
||||
clientId: 'client-id',
|
||||
@ -60,6 +60,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');
|
||||
|
@ -3,8 +3,8 @@
|
||||
<div class="login">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12">
|
||||
<button class="btn btn-block btn-default" ng-click="authenticate('ping')">
|
||||
Login with Meechum
|
||||
<button class="btn btn-block btn-default" ng-click="authenticate('Example')">
|
||||
Login with Example
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,18 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('lemur')
|
||||
.config(function config($routeProvider) {
|
||||
$routeProvider.when('/unlock', {
|
||||
templateUrl: '/angular/authentication/unlock/unlock.tpl.html',
|
||||
controller: 'UnlockCtrl'
|
||||
});
|
||||
})
|
||||
.controller('UnlockCtrl', function ($scope, $location, lemurRestangular, messageService) {
|
||||
$scope.unlock = function () {
|
||||
lemurRestangular.one('unlock').customPOST({'password': $scope.password})
|
||||
.then(function (data) {
|
||||
messageService.addMessage(data);
|
||||
$location.path('/dashboard');
|
||||
});
|
||||
};
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
<h2 class="featurette-heading">Unlock <span class="text-muted"><small>Assume 9 is twice 5; how will you write 6 times 5 in the same system of notation?</small></span></h2>
|
||||
<form class="form-horizontal" _lpchecked="1">
|
||||
<fieldset class="col-lg-offset-4">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-4">
|
||||
<input type="password" ng-model="password" placeholder="Password" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="featurette-divider">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-4">
|
||||
<button ng-click="unlock()" class="btn btn-success">Unlock</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
@ -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;
|
||||
|
@ -1,9 +1,32 @@
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<div class="modal-header">Edit Authority <span class="text-muted"><small>chain of command!</small></span></div>
|
||||
<h3 class="modal-header">Edit <span class="text-muted"><small>{{ authority.name }}</small></span></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="createForm" class="form-horizontal" role="form" novalidate>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.owner.$invalid, 'has-success': !editForm.owner.$invalid&&editForm.owner.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Owner
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<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
|
||||
email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': editForm.description.$invalid, 'has-success': !editForm.$invalid&&editForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="authority.description" placeholder="Something elegant" class="form-control" required></textarea>
|
||||
<p ng-show="editForm.description.$invalid && !editForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-sm-2">
|
||||
Roles
|
@ -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>
|
||||
@ -26,8 +26,8 @@
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="caDescription" ng-model="authority.caDescription" placeholder="Something elegant" class="form-control" ng-maxlength="250" ng-pattern="/^[\w\-\s]+$/" required></textarea>
|
||||
<p ng-show="trackingForm.caDescription.$invalid && !trackingForm.caDescription.$pristine" class="help-block">You must give a short description about this authority will be used for, it should contain only alphanumeric characters</p>
|
||||
<textarea name="caDescription" ng-model="authority.caDescription" placeholder="Something elegant" class="form-control" ng-maxlength="250" required></textarea>
|
||||
<p ng-show="trackingForm.caDescription.$invalid && !trackingForm.caDescription.$pristine" class="help-block">You must give a short description about this authority will be used for</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
@ -36,8 +36,8 @@
|
||||
Common Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="commonName" ng-model="authority.caDN.commonName" placeholder="Common Name" class="form-control" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
|
||||
<input name="commonName" ng-model="authority.caDN.commonName" placeholder="Common Name" class="form-control" ng-maxlength="64" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name and it must be less than 64 characters in length</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
|
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;
|
||||
|
@ -46,7 +46,7 @@ angular.module('lemur')
|
||||
$scope.edit = function (authorityId) {
|
||||
var modalInstance = $modal.open({
|
||||
animation: true,
|
||||
templateUrl: '/angular/authorities/authority/authorityEdit.tpl.html',
|
||||
templateUrl: '/angular/authorities/authority/edit.tpl.html',
|
||||
controller: 'AuthorityEditController',
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
@ -62,6 +62,25 @@ angular.module('lemur')
|
||||
|
||||
};
|
||||
|
||||
$scope.editRole = function (roleId) {
|
||||
var modalInstance = $modal.open({
|
||||
animation: true,
|
||||
templateUrl: '/angular/roles/role/role.tpl.html',
|
||||
controller: 'RolesEditController',
|
||||
size: 'lg',
|
||||
resolve: {
|
||||
editId: function () {
|
||||
return roleId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
modalInstance.result.then(function () {
|
||||
$scope.authoritiesTable.reload();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.create = function () {
|
||||
var modalInstance = $modal.open({
|
||||
animation: true,
|
||||
|
@ -29,7 +29,7 @@
|
||||
</td>
|
||||
<td data-title="'Roles'"> <!--filter="{ 'select': 'role' }" filter-data="roleService.getRoleDropDown()">-->
|
||||
<div class="btn-group">
|
||||
<a href="#/roles/{{ role.id }}/edit" ng-repeat="role in authority.roles" class="btn btn-sm btn-danger">
|
||||
<a ng-click="editRole(role.id)" ng-repeat="role in authority.roles" class="btn btn-sm btn-danger">
|
||||
{{ role.name }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -25,6 +25,9 @@ angular.module('lemur')
|
||||
.controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular, NotificationService) {
|
||||
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
|
||||
|
||||
// set the defaults
|
||||
CertificateService.getDefaults($scope.certificate);
|
||||
|
||||
$scope.create = function (certificate) {
|
||||
WizardHandler.wizard().context.loading = true;
|
||||
CertificateService.create(certificate).then(function () {
|
||||
|
@ -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"
|
||||
|
@ -6,7 +6,7 @@
|
||||
Country
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="country" ng-model="certificate.country" placeholder="Country" class="form-control" ng-init="certificate.country = 'US'" required/>
|
||||
<input name="country" ng-model="certificate.country" placeholder="Country" class="form-control" required/>
|
||||
<p ng-show="dnForm.country.$invalid && !dnForm.country.$pristine" class="help-block">You must enter a country</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -16,7 +16,7 @@
|
||||
State
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="state" ng-model="certificate.state" placeholder="State" class="form-control" ng-init="certificate.state = 'California'" required/>
|
||||
<input name="state" ng-model="certificate.state" placeholder="State" class="form-control" required/>
|
||||
<p ng-show="dnForm.state.$invalid && !dnForm.state.$pristine" class="help-block">You must enter a state</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,7 +26,7 @@
|
||||
Location
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="location" ng-model="certificate.location" placeholder="Location" class="form-control" ng-init="certificate.location = 'Los Gatos'"required/>
|
||||
<input name="location" ng-model="certificate.location" placeholder="Location" class="form-control" required/>
|
||||
<p ng-show="dnForm.location.$invalid && !dnForm.location.$pristine" class="help-block">You must enter a location</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -36,7 +36,7 @@
|
||||
Organization
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organization" ng-model="certificate.organization" placeholder="Organization" class="form-control" ng-init="certificate.organization = 'Netflix'" required/>
|
||||
<input name="organization" ng-model="certificate.organization" placeholder="Organization" class="form-control" required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organization.$pristine" class="help-block">You must enter a organization</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,7 +46,7 @@
|
||||
Organizational Unit
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="organizationalUnit" ng-model="certificate.organizationalUnit" placeholder="Organizational Unit" class="form-control" ng-init="certificate.organizationalUnit = 'Operations'"required/>
|
||||
<input name="organizationalUnit" ng-model="certificate.organizationalUnit" placeholder="Organizational Unit" class="form-control" required/>
|
||||
<p ng-show="dnForm.organization.$invalid && !dnForm.organizationalUnit.$pristine" class="help-block">You must enter a organizational unit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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
|
||||
|
@ -94,6 +94,11 @@
|
||||
<input type="checkbox" ng-model="certificate.extensions.extendedKeyUsage.useServerAuthentication">Server Authentication
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="certificate.extensions.extendedKeyUsage.useClientAuthentication">Client Authentication
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="certificate.extensions.extendedKeyUsage.useEmail">Email
|
||||
|
@ -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>
|
||||
@ -16,8 +16,8 @@
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
|
||||
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" required></textarea>
|
||||
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
@ -47,8 +47,8 @@
|
||||
Common Name
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next panel" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
|
||||
<input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next few panels" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" ng-maxlength="64" required/>
|
||||
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name and it must be less than 64 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -11,21 +11,30 @@
|
||||
</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
|
||||
email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': uploadForm.name.$invalid, 'has-success': !uploadForm.name.$invalid&&uploadForm.name.$dirty}">
|
||||
<label class="control-label col-sm-2" tooltip="If no name is provided, Lemur will generate a name for you">
|
||||
Custom Name <span class="glyphicon glyphicon-question-sign"></span>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="name" ng-model="certificate.name" placeholder="the.example.net-SymantecCorporation-20150828-20160830" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group"
|
||||
ng-class="{'has-error': uploadForm.description.$invalid, 'has-success': !uploadForm.$invalid&&uploadForm.description.$dirty}">
|
||||
<label class="control-label col-sm-2">
|
||||
Description
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
|
||||
<p ng-show="uploadForm.description.$invalid && !uploadForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
|
||||
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" required></textarea>
|
||||
<p ng-show="uploadForm.description.$invalid && !uploadForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for.</p>
|
||||
</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})
|
||||
@ -107,7 +107,6 @@ angular.module('lemur')
|
||||
title: certificate.name,
|
||||
body: 'Successfully created!'
|
||||
});
|
||||
$location.path('/certificates');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -120,14 +119,21 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
CertificateService.update = function (certificate) {
|
||||
return LemurRestangular.copy(certificate).put().then(function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: certificate.name,
|
||||
body: 'Successfully updated!'
|
||||
return LemurRestangular.copy(certificate).put().then(
|
||||
function () {
|
||||
toaster.pop({
|
||||
type: 'success',
|
||||
title: certificate.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
type: 'error',
|
||||
title: certificate.name,
|
||||
body: 'Failed to update ' + response.data.message
|
||||
});
|
||||
});
|
||||
$location.path('certificates');
|
||||
});
|
||||
};
|
||||
|
||||
CertificateService.upload = function (certificate) {
|
||||
@ -138,7 +144,6 @@ angular.module('lemur')
|
||||
title: certificate.name,
|
||||
body: 'Successfully uploaded!'
|
||||
});
|
||||
$location.path('/certificates');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -201,6 +206,16 @@ angular.module('lemur')
|
||||
});
|
||||
};
|
||||
|
||||
CertificateService.getDefaults = function (certificate) {
|
||||
return DefaultService.get().then(function (defaults) {
|
||||
certificate.country = defaults.country;
|
||||
certificate.state = defaults.state;
|
||||
certificate.location = defaults.location;
|
||||
certificate.organization = defaults.organization;
|
||||
certificate.organizationalUnit = defaults.organizationalUnit;
|
||||
});
|
||||
};
|
||||
|
||||
CertificateService.updateActive = function (certificate) {
|
||||
return certificate.put().then(
|
||||
function () {
|
||||
|
@ -34,16 +34,6 @@ angular.module('lemur')
|
||||
});
|
||||
});
|
||||
|
||||
PluginService.getByType('destination').then(function (plugins) {
|
||||
$scope.plugins = plugins;
|
||||
_.each($scope.plugins, function (plugin) {
|
||||
if (plugin.slug === $scope.destination.pluginName) {
|
||||
plugin.pluginOptions = $scope.destination.destinationOptions;
|
||||
$scope.destination.plugin = plugin;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.save = function (destination) {
|
||||
DestinationService.update(destination).then(function () {
|
||||
$modalInstance.close();
|
||||
|
@ -21,7 +21,7 @@ angular.module('lemur')
|
||||
}, {
|
||||
total: 0, // length of data
|
||||
getData: function ($defer, params) {
|
||||
DomainApi.getList().then(function (data) {
|
||||
DomainApi.getList(params.url()).then(function (data) {
|
||||
params.total(data.total);
|
||||
$defer.resolve(data);
|
||||
});
|
||||
|
6
lemur/static/app/angular/roles/role/role.js
vendored
@ -18,6 +18,12 @@ angular.module('lemur')
|
||||
$modalInstance.dismiss('cancel');
|
||||
};
|
||||
|
||||
$scope.userPage = 1;
|
||||
$scope.loadMoreRoles = function () {
|
||||
$scope.userPage += 1;
|
||||
RoleService.loadMoreUsers($scope.role, $scope.userPage);
|
||||
};
|
||||
|
||||
$scope.userService = UserService;
|
||||
$scope.roleService = RoleService;
|
||||
})
|
||||
|
@ -27,7 +27,7 @@
|
||||
Username
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input ng-show="!role.fromServer" name="username" ng-model="role.username" placeholder="Username" class="form-control" required/>
|
||||
<input ng-show="!role.fromServer" name="username" ng-model="role.username" placeholder="Username" class="form-control"/>
|
||||
<div class="well">
|
||||
<span ng-show="role.password">
|
||||
{{ role.username }}
|
||||
@ -43,7 +43,7 @@
|
||||
Password
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<input ng-show="!role.fromServer" type="password" name="password" ng-model="role.password" placeholder="hunter2" class="form-control" required/>
|
||||
<input ng-show="!role.fromServer" type="password" name="password" ng-model="role.password" placeholder="hunter2" class="form-control"/>
|
||||
<p ng-show="createForm.password.$invalid && !createForm.password.$pristine" class="help-block">You must enter an password</p>
|
||||
<div class="well">
|
||||
<span ng-show="role.password">
|
||||
@ -61,7 +61,7 @@
|
||||
<input tooltip="You can attach any user to this role, once attached they will have access as defined by this role"
|
||||
typeahead="user.username for user in userService.findUserByName($viewValue)" typeahead-loading="loadingUsers"
|
||||
typeahead-min-wait="100" typeahead-on-select="role.addUser($item)"
|
||||
type="text" name="user" ng-model="role.selectedUser" placeholder="Username..." class="form-control" required/>
|
||||
type="text" name="user" ng-model="role.selectedUser" placeholder="Username..." class="form-control"/>
|
||||
<table ng-show="role.users" class="table">
|
||||
<tr ng-repeat="user in role.users track by $index">
|
||||
<td>{{ user.username }}</td>
|
||||
@ -69,6 +69,10 @@
|
||||
<button type="button" ng-click="role.removeUser($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><a class="pull-right" ng-click="loadMoreUsers()"><strong>More</strong></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
12
lemur/static/app/angular/roles/services.js
vendored
@ -34,11 +34,19 @@ angular.module('lemur')
|
||||
};
|
||||
|
||||
RoleService.getUsers = function (role) {
|
||||
role.customGET('users').then(function (users) {
|
||||
return role.getList('users').then(function (users) {
|
||||
role.users = users;
|
||||
});
|
||||
};
|
||||
|
||||
RoleService.loadMoreUsers = function (role, page) {
|
||||
role.getList('users', {page: page}).then(function (users) {
|
||||
_.each(users, function (user) {
|
||||
role.users.push(user);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
RoleService.create = function (role) {
|
||||
return RoleApi.post(role).then(
|
||||
function () {
|
||||
@ -47,7 +55,6 @@ angular.module('lemur')
|
||||
title: role.name,
|
||||
body: 'Has been successfully created!'
|
||||
});
|
||||
$location.path('roles');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
@ -66,7 +73,6 @@ angular.module('lemur')
|
||||
title: role.name,
|
||||
body: 'Successfully updated!'
|
||||
});
|
||||
$location.path('roles');
|
||||
},
|
||||
function (response) {
|
||||
toaster.pop({
|
||||
|
@ -1,19 +1,12 @@
|
||||
<div class="jumbotron">
|
||||
<h1>Hey there!</h1>
|
||||
|
||||
<p>Welcome to Lemur! A central portal for all (most) of your SSL needs.</p>
|
||||
<p>Welcome to Lemur! A central portal for all (most) of your TLS needs.</p>
|
||||
|
||||
<p><a href="/#/certificates/create" class="btn btn-primary btn-lg" role="button">Create a Certificate</a></p>
|
||||
</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>
|
||||
<h2 class="featurette-heading">TLS In The Cloud <span class="text-muted">Encrypt it all </span></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -87,7 +87,7 @@
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="text-muted">Lemur is maintained by <a href="mailto:secops@netflix.com">Security Operations</a>.</p>
|
||||
<p class="text-muted">Lemur is broken regularly by <a href="https://github.com/Netflix/lemur.git">Netflix</a>.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
@ -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"})
|
@ -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,12 +30,15 @@ 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`.
|
||||
"""
|
||||
_app = create_app(os.path.dirname(os.path.realpath(__file__)) + '/conf.py')
|
||||
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()
|
||||
|
||||
|
@ -15,6 +15,7 @@ def get_key():
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY')
|
||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip()
|
||||
except RuntimeError:
|
||||
print("No Encryption Key Found")
|
||||
return ''
|
||||
|
@ -10,3 +10,6 @@ exclude = .tox,.git,*/migrations/*,lemur/static/*,docs/*
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
description-file = README.rst
|
||||
|
51
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.0dev',
|
||||
'cryptography==1.0.1',
|
||||
'pyopenssl==0.15.1',
|
||||
'pyjwt==1.0.1',
|
||||
'xmltodict==0.9.2',
|
||||
@ -72,7 +74,7 @@ class SmartInstall(install):
|
||||
`build_static` which is required for JavaScript assets and other things.
|
||||
"""
|
||||
def _needs_static(self):
|
||||
return not os.path.exists(os.path.join(ROOT, 'lemur-package.json'))
|
||||
return not os.path.exists(os.path.join(ROOT, 'lemur/static/dist'))
|
||||
|
||||
def run(self):
|
||||
if self._needs_static():
|
||||
@ -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]")
|
||||
check_output(['npm', 'install', '--quiet'], cwd=ROOT)
|
||||
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)
|
||||
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.4',
|
||||
author='Kevin Glisson',
|
||||
author_email='kglisson@netflix.com',
|
||||
long_description=open('README.rst').read(),
|
||||
packages=['lemur'],
|
||||
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=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=install_requires,
|
||||
@ -125,10 +142,8 @@ setup(
|
||||
},
|
||||
cmdclass={
|
||||
'build_static': BuildStatic,
|
||||
'develop': DevelopWithBuildStatic,
|
||||
'sdist': SdistWithBuildStatic,
|
||||
'install': SmartInstall
|
||||
|
||||
},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
@ -136,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',
|
||||
|