Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
bb5b32a435 | |||
da75d31fac | |||
b295679cc3 | |||
e3db887b07 | |||
5d61ed4d5b | |||
ba1c549070 | |||
f4178fefd2 | |||
3a757f8f94 | |||
2bb1d9ee21 | |||
28a4d21bcc | |||
49800bf9da | |||
2c081df06b | |||
1636847040 | |||
c977826d62 | |||
dbea35ba19 | |||
91d0b36a6a | |||
890a016ee0 | |||
35a933ce9b | |||
acf6ac1531 | |||
ad742e6eee | |||
f7938bf226 | |||
deb7586372 | |||
2da9754ffa | |||
4f409547a0 | |||
c1168399a4 | |||
a40298df08 | |||
0175df821c | |||
b5c38c2854 | |||
dc1f1c247a | |||
28b9a73a83 | |||
d097da685a | |||
1c137e6596 | |||
0d388a85bb | |||
a0a5e66cc3 | |||
1d486cf1fd | |||
377ba25413 |
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# These owners will be the default owners for everything in the repo.
|
||||
* @hosseinsh @csine-nflx @charhate @jtschladen
|
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
version: 2
|
||||
updates:
|
||||
- directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "08:00"
|
||||
timezone: "America/Los_Angeles"
|
||||
package-ecosystem: "pip"
|
||||
reviewers:
|
||||
- "hosseinsh"
|
||||
- "csine-nflx"
|
||||
- "charhate"
|
||||
- "jtschladen"
|
||||
versioning-strategy: lockfile-only
|
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '15 16 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Install prerequisites for python-ldap. See: https://www.python-ldap.org/en/python-ldap-3.3.0/installing.html#build-prerequisites
|
||||
- name: Install python-ldap prerequisites
|
||||
run: sudo apt-get install libldap2-dev libsasl2-dev
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
14
.github/workflows/dependabot-auto-merge.yml
vendored
Normal file
14
.github/workflows/dependabot-auto-merge.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: dependabot-auto-merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ahmadnassri/action-dependabot-auto-merge@v2
|
||||
with:
|
||||
target: minor
|
||||
github-token: ${{ secrets.DEPENDABOT_GITHUB_TOKEN }}
|
@ -1,6 +1,13 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
0.9.0 - `2021-03-17`
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This release fixes three critical vulnerabilities where an authenticated user could retrieve/access
|
||||
unauthorized information. (Issue `#3463 <https://github.com/Netflix/lemur/issues/3463>`_)
|
||||
|
||||
|
||||
0.8.1 - `2021-03-12`
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -1,10 +1,18 @@
|
||||
Doing a release
|
||||
===============
|
||||
|
||||
Doing a release of ``lemur`` requires a few steps.
|
||||
Doing a release of ``lemur`` is now mostly automated and consists of the following steps:
|
||||
|
||||
Bumping the version number
|
||||
--------------------------
|
||||
* Raise a PR to add the release date and summary in the :doc:`/changelog`.
|
||||
* Merge above PR and create a new `Github release <https://github.com/Netflix/lemur/releaes>`_: set the tag starting with v, e.g., v0.9.0
|
||||
|
||||
The `publish workflow <https://github.com/Netflix/lemur/actions/workflows/lemur-publish-release-pypi.yml>`_ uses the git
|
||||
tag to set the release version.
|
||||
|
||||
The following describes the manual release steps, which is now obsolete:
|
||||
|
||||
Manually Bumping the version number
|
||||
-----------------------------------
|
||||
|
||||
The next step in doing a release is bumping the version number in the
|
||||
software.
|
||||
@ -14,8 +22,8 @@ software.
|
||||
* Do a commit indicating this, and raise a pull request with this.
|
||||
* Wait for it to be merged.
|
||||
|
||||
Performing the release
|
||||
----------------------
|
||||
Manually Performing the release
|
||||
-------------------------------
|
||||
|
||||
The commit that merged the version number bump is now the official release
|
||||
commit for this release. You need an `API key <https://pypi.org/manage/account/#api-tokens>`_,
|
||||
|
@ -600,3 +600,118 @@ Using `python-jwt` converting an existing private key in PEM format is quite eas
|
||||
{"body": {}, "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/<ACCOUNT_NUMBER>"}
|
||||
|
||||
The URI can be retrieved from the ACME create account endpoint when creating a new account, using the existing key.
|
||||
|
||||
OpenSSH
|
||||
=======
|
||||
|
||||
OpenSSH (also known as OpenBSD Secure Shell) is a suite of secure networking utilities based on the Secure Shell (SSH) protocol, which provides a secure channel over an unsecured network in a client–server architecture.
|
||||
|
||||
Using a PKI with OpenSSH means you can sign a key for a user and it can log into any server that trust the CA.
|
||||
|
||||
Using a CA avoids TOFU or synchronize a list of server public keys to `known_hosts` files.
|
||||
|
||||
This is useful when you're managing large number of machines or for an immutable infrastructure.
|
||||
|
||||
Add first OpenSSH authority
|
||||
---------------------------
|
||||
|
||||
To start issuing OpenSSH, you need to create an OpenSSH authority. To do this, visit
|
||||
Authorities -> Create. Set the applicable attributes:
|
||||
|
||||
- Name : OpenSSH
|
||||
- Common Name: example.net
|
||||
|
||||
Then click "More Options" and change the plugin value to "OpenSSH".
|
||||
|
||||
Just click to "Create" button to add this authority.
|
||||
|
||||
.. note:: OpenSSH do not support sub CA feature.
|
||||
|
||||
Add a server certificate
|
||||
-------------------------
|
||||
|
||||
Now visit Certificates -> Create to add a server certificate. Set the applicable attributes:
|
||||
|
||||
- Common Name: server.example.net
|
||||
|
||||
Then click "More Options" and set the Certificate Template to "Server Certificate".
|
||||
|
||||
This step is important, a certificat for a server and for a client is not exactly the same thing.
|
||||
In this case "Common Name" and all Subject Alternate Names with type DNSName will be added in the certificate.
|
||||
|
||||
Finally click on "Create" button.
|
||||
|
||||
Add a client certificate
|
||||
------------------------
|
||||
|
||||
Now visit Certificates -> Create to add a client certificate. Set the applicable attributes:
|
||||
|
||||
- Common Name: example.net
|
||||
|
||||
Then click "More Options" and set the Certificate Template to "Client Certificate".
|
||||
|
||||
In this case the name of the creator is used as principal (in this documentation we assume that this certificate is created by the user "lemur").
|
||||
|
||||
Finally click on "Create" button.
|
||||
|
||||
Configure OpenSSH server
|
||||
------------------------
|
||||
|
||||
Connect to the server.example.net server to correctly configure the OpenSSH server with the CA created previously.
|
||||
|
||||
First of all add the CA chain, private and public certificates:
|
||||
|
||||
- Create file `/etc/ssh/ca.pub` and copy the "CHAIN" content of the *server certificate* (everything in one line).
|
||||
- Create file `/etc/ssh/ssh_host_key` and copy "PRIVATE KEY" content.
|
||||
- Create file `/etc/ssh/ssh_host_key.pub` and copy "PUBLIC CERTIFICATE" content (everything in one line).
|
||||
|
||||
Set the appropriate right:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
chmod 600 /etc/ssh/ca.pub /etc/ssh/ssh_host_key
|
||||
chmod 644 /etc/ssh/ssh_host_key.pub
|
||||
chown root: /etc/ssh/ca.pub /etc/ssh/ssh_host_key /etc/ssh/ssh_host_key.pub
|
||||
|
||||
Then change OpenSSH server configuration to use these files. Edit `/etc/ssh/sshd_config` and add::
|
||||
|
||||
TrustedUserCAKeys /etc/ssh/ca.pub
|
||||
HostKey /etc/ssh/ssh_host_key
|
||||
HostCertificate /etc/ssh/ssh_host_key.pub
|
||||
|
||||
You can remove all other `HostKey` lines.
|
||||
|
||||
Finally restart OpenSSH.
|
||||
|
||||
Configure the OpenSSH client
|
||||
----------------------------
|
||||
|
||||
Now you can configure the user's computer.
|
||||
|
||||
First of all add private and public certificates:
|
||||
|
||||
- Create file `~/.ssh/key` and copy "PRIVATE KEY" content.
|
||||
- Create file `~/.ssh/key.pub` and copy "PUBLIC CERTIFICATE" content of the *client certicate* (everything in one line).
|
||||
|
||||
Set the appropriate right:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
chmod 600 ~/.ssh/key.pub ~/.ssh/key
|
||||
|
||||
To avoid TOFU, edite the `~/.ssh/known_hosts` file and add a new line (all in one line):
|
||||
|
||||
- @cert-authority \*example.net
|
||||
- the "CHAIN" content
|
||||
|
||||
Now you can connect to server with (here 'lemur' is the principal name and must exists on the server):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ssh lemur@server.example.net -i ~/.ssh/key
|
||||
|
||||
With this configuration you don't have any line like::
|
||||
|
||||
Warning: Permanently added 'server.example.net,192.168.0.1' (RSA) to the list of known hosts.
|
||||
|
||||
And you don't have to enter any password.
|
||||
|
@ -7,7 +7,7 @@
|
||||
"""
|
||||
from flask import current_app
|
||||
|
||||
from marshmallow import fields, validates_schema, pre_load
|
||||
from marshmallow import fields, validates_schema, pre_load, post_dump
|
||||
from marshmallow import validate
|
||||
from marshmallow.exceptions import ValidationError
|
||||
|
||||
@ -24,6 +24,7 @@ from lemur.common import validators, missing
|
||||
|
||||
from lemur.common.fields import ArrowDateTime
|
||||
from lemur.constants import CERTIFICATE_KEY_TYPES
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
class AuthorityInputSchema(LemurInputSchema):
|
||||
@ -129,6 +130,12 @@ class AuthorityOutputSchema(LemurOutputSchema):
|
||||
default_validity_days = fields.Integer()
|
||||
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema)
|
||||
|
||||
@post_dump
|
||||
def handle_auth_certificate(self, cert):
|
||||
# Plugins may need to modify the cert object before returning it to the user
|
||||
plugin = plugins.get(cert['plugin']['slug'])
|
||||
plugin.wrap_auth_certificate(cert['authority_certificate'])
|
||||
|
||||
|
||||
class AuthorityNestedOutputSchema(LemurOutputSchema):
|
||||
__envelope__ = False
|
||||
|
@ -117,6 +117,12 @@ def create(**kwargs):
|
||||
"""
|
||||
Creates a new authority.
|
||||
"""
|
||||
ca_name = kwargs.get("name")
|
||||
if get_by_name(ca_name):
|
||||
raise Exception(f"Authority with name {ca_name} already exists")
|
||||
if role_service.get_by_name(f"{ca_name}_admin") or role_service.get_by_name(f"{ca_name}_operator"):
|
||||
raise Exception(f"Admin and/or operator roles for authority {ca_name} already exist")
|
||||
|
||||
body, private_key, chain, roles = mint(**kwargs)
|
||||
|
||||
kwargs["creator"].roles = list(set(list(kwargs["creator"].roles) + roles))
|
||||
|
@ -38,6 +38,7 @@ from lemur.schemas import (
|
||||
AssociatedRotationPolicySchema,
|
||||
)
|
||||
from lemur.users.schemas import UserNestedOutputSchema
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
class CertificateSchema(LemurInputSchema):
|
||||
@ -324,6 +325,8 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
||||
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||
authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||
# if this certificate is an authority, the authority informations are in root_authority
|
||||
root_authority = fields.Nested(AuthorityNestedOutputSchema)
|
||||
dns_provider = fields.Nested(DnsProvidersNestedOutputSchema)
|
||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||
@ -357,6 +360,22 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||
if field in data and data[field] is None:
|
||||
data.pop(field)
|
||||
|
||||
@post_dump
|
||||
def handle_certificate(self, cert):
|
||||
# Plugins may need to modify the cert object before returning it to the user
|
||||
if cert['authority'] is None:
|
||||
if cert['root_authority'] is None:
|
||||
plugin = None
|
||||
else:
|
||||
# this certificate is an authority
|
||||
plugin = plugins.get(cert['root_authority']['plugin']['slug'])
|
||||
else:
|
||||
plugin = plugins.get(cert['authority']['plugin']['slug'])
|
||||
if plugin:
|
||||
plugin.wrap_certificate(cert)
|
||||
if 'root_authority' in cert:
|
||||
del cert['root_authority']
|
||||
|
||||
|
||||
class CertificateShortOutputSchema(LemurOutputSchema):
|
||||
id = fields.Integer()
|
||||
|
@ -88,6 +88,16 @@ def get_by_attributes(conditions):
|
||||
return database.find_all(query, Certificate, conditions).all()
|
||||
|
||||
|
||||
def get_by_root_authority(id):
|
||||
"""
|
||||
Retrieves certificate by its root_authority's id.
|
||||
|
||||
:param id:
|
||||
:return:
|
||||
"""
|
||||
return database.get(Certificate, id, field="root_authority_id")
|
||||
|
||||
|
||||
def delete(cert_id):
|
||||
"""
|
||||
Delete's a certificate.
|
||||
@ -679,7 +689,16 @@ def stats(**kwargs):
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
if kwargs.get("metric") == "not_after":
|
||||
|
||||
# Verify requested metric
|
||||
allow_list = ["bits", "issuer", "not_after", "signing_algorithm"]
|
||||
req_metric = kwargs.get("metric")
|
||||
if req_metric not in allow_list:
|
||||
raise Exception(
|
||||
f"Stats not available for requested metric: {req_metric}"
|
||||
)
|
||||
|
||||
if req_metric == "not_after":
|
||||
start = arrow.utcnow()
|
||||
end = start.shift(weeks=+32)
|
||||
items = (
|
||||
@ -691,7 +710,7 @@ def stats(**kwargs):
|
||||
)
|
||||
|
||||
else:
|
||||
attr = getattr(Certificate, kwargs.get("metric"))
|
||||
attr = getattr(Certificate, req_metric)
|
||||
query = database.db.session.query(attr, func.count(attr))
|
||||
|
||||
items = query.group_by(attr).all()
|
||||
|
@ -33,6 +33,7 @@ from lemur.certificates.schemas import (
|
||||
|
||||
from lemur.roles import service as role_service
|
||||
from lemur.logs import service as log_service
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
||||
mod = Blueprint("certificates", __name__)
|
||||
@ -635,7 +636,12 @@ class CertificatesStats(AuthenticatedResource):
|
||||
|
||||
args = self.reqparse.parse_args()
|
||||
|
||||
items = service.stats(**args)
|
||||
try:
|
||||
items = service.stats(**args)
|
||||
except Exception as e:
|
||||
sentry.captureException()
|
||||
return dict(message=f"Failed to retrieve stats: {str(e)}"), 400
|
||||
|
||||
return dict(items=items, total=len(items))
|
||||
|
||||
|
||||
@ -686,6 +692,16 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||
return dict(message="You are not authorized to view this key"), 403
|
||||
|
||||
log_service.create(g.current_user, "key_view", certificate=cert)
|
||||
|
||||
# Plugins may need to modify the cert object before returning it to the user
|
||||
if cert.root_authority:
|
||||
# this certificate is an authority
|
||||
plugin_name = cert.root_authority.plugin_name
|
||||
else:
|
||||
plugin_name = cert.authority.plugin_name
|
||||
plugin = plugins.get(plugin_name)
|
||||
plugin.wrap_private_key(cert)
|
||||
|
||||
response = make_response(jsonify(key=cert.private_key), 200)
|
||||
response.headers["cache-control"] = "private, max-age=0, no-cache, no-store"
|
||||
response.headers["pragma"] = "no-cache"
|
||||
|
@ -425,7 +425,7 @@ class CertificateDestinations(AuthenticatedResource):
|
||||
|
||||
|
||||
class DestinationsStats(AuthenticatedResource):
|
||||
""" Defines the 'certificates' stats endpoint """
|
||||
""" Defines the 'destinations' stats endpoint """
|
||||
|
||||
def __init__(self):
|
||||
self.reqparse = reqparse.RequestParser()
|
||||
|
@ -10,9 +10,9 @@ class DnsProvidersNestedOutputSchema(LemurOutputSchema):
|
||||
name = fields.String()
|
||||
provider_type = fields.String()
|
||||
description = fields.String()
|
||||
credentials = fields.String()
|
||||
api_endpoint = fields.String()
|
||||
date_created = ArrowDateTime()
|
||||
# credentials are intentionally omitted (they are input-only)
|
||||
|
||||
|
||||
class DnsProvidersNestedInputSchema(LemurInputSchema):
|
||||
|
@ -31,3 +31,12 @@ class IssuerPlugin(Plugin):
|
||||
|
||||
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def wrap_certificate(self, cert):
|
||||
pass
|
||||
|
||||
def wrap_auth_certificate(self, cert):
|
||||
pass
|
||||
|
||||
def wrap_private_key(self, cert):
|
||||
pass
|
||||
|
4
lemur/plugins/lemur_openssh/__init__.py
Normal file
4
lemur/plugins/lemur_openssh/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
try:
|
||||
VERSION = __import__("pkg_resources").get_distribution(__name__).version
|
||||
except Exception as e:
|
||||
VERSION = "unknown"
|
159
lemur/plugins/lemur_openssh/plugin.py
Normal file
159
lemur/plugins/lemur_openssh/plugin.py
Normal file
@ -0,0 +1,159 @@
|
||||
"""
|
||||
.. module: lemur.plugins.lemur_openssh.plugin
|
||||
:platform: Unix
|
||||
:copyright: (c) 2020 by Emmanuel Garette, see AUTHORS for more
|
||||
:license: Apache, see LICENSE for more details.
|
||||
|
||||
.. moduleauthor:: Emmanuel Garette <gnunux@gnunux.info>
|
||||
"""
|
||||
import subprocess
|
||||
from os import unlink
|
||||
|
||||
from flask import current_app
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from datetime import datetime
|
||||
|
||||
from lemur.utils import mktempfile
|
||||
from lemur.plugins import lemur_openssh as openssh
|
||||
from lemur.common.utils import parse_private_key, parse_certificate
|
||||
from lemur.plugins.lemur_cryptography.plugin import CryptographyIssuerPlugin
|
||||
from lemur.certificates.service import get_by_root_authority
|
||||
|
||||
|
||||
def run_process(command):
|
||||
"""
|
||||
Runs a given command with pOpen and wraps some
|
||||
error handling around it.
|
||||
:param command:
|
||||
:return:
|
||||
"""
|
||||
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
current_app.logger.debug(" ".join(command))
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
current_app.logger.error(stderr.decode())
|
||||
raise Exception(stderr.decode())
|
||||
|
||||
|
||||
def split_cert(body):
|
||||
"""
|
||||
To display certificate in Lemur website, we have to split
|
||||
certificate in several line
|
||||
:param body: certificate
|
||||
:retur: splitted certificate
|
||||
"""
|
||||
length = 65
|
||||
return '\n'.join([body[i:i + length] for i in range(0, len(body), length)])
|
||||
|
||||
|
||||
def sign_certificate(common_name, public_key, authority_private_key, user, extensions, not_before, not_after):
|
||||
private_key = parse_private_key(authority_private_key).private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.OpenSSH,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
).decode()
|
||||
with mktempfile() as issuer_tmp:
|
||||
cmd = ['ssh-keygen', '-s', issuer_tmp]
|
||||
with open(issuer_tmp, 'w') as i:
|
||||
i.writelines(private_key)
|
||||
if 'extendedKeyUsage' in extensions and extensions['extendedKeyUsage'].get('useClientAuthentication'):
|
||||
cmd.extend(['-I', user['username'] + ' user key',
|
||||
'-n', user['username']])
|
||||
else:
|
||||
domains = {common_name}
|
||||
for name in extensions['subAltNames']['names']:
|
||||
if name['nameType'] == 'DNSName':
|
||||
domains.add(name['value'])
|
||||
cmd.extend(['-I', common_name + ' host key',
|
||||
'-n', ','.join(domains),
|
||||
'-h'])
|
||||
# something like 20201024102030
|
||||
ssh_not_before = datetime.fromisoformat(not_before).strftime("%Y%m%d%H%M%S")
|
||||
ssh_not_after = datetime.fromisoformat(not_after).strftime("%Y%m%d%H%M%S")
|
||||
cmd.extend(['-V', ssh_not_before + ':' + ssh_not_after])
|
||||
with mktempfile() as cert_tmp:
|
||||
with open(cert_tmp, 'w') as f:
|
||||
f.write(public_key)
|
||||
|
||||
cmd.append(cert_tmp)
|
||||
run_process(cmd)
|
||||
pub = cert_tmp + '-cert.pub'
|
||||
with open(pub, 'r') as p:
|
||||
body = split_cert(p.read())
|
||||
unlink(pub)
|
||||
return body
|
||||
|
||||
|
||||
class OpenSSHIssuerPlugin(CryptographyIssuerPlugin):
|
||||
"""This issuer plugins is base in Cryptography plugin
|
||||
Certificates and authorities are x509 certificates created by Cryptography plugin.
|
||||
Those certificates are converted to OpenSSH format when people get them.
|
||||
"""
|
||||
title = "OpenSSH"
|
||||
slug = "openssh-issuer"
|
||||
description = "Enables the creation and signing OpenSSH keys"
|
||||
version = openssh.VERSION
|
||||
|
||||
author = "Emmanuel Garette"
|
||||
author_url = "http://gnunux.info"
|
||||
|
||||
def create_authority(self, options):
|
||||
# OpenSSH do not support parent's authoriy
|
||||
if options.get("parent"):
|
||||
raise Exception('cannot create authority with a parent for OpenSSH plugin')
|
||||
# create a x509 certificat
|
||||
cert_pem, private_key, chain_cert_pem, roles = super().create_authority(options)
|
||||
return cert_pem, private_key, chain_cert_pem, roles
|
||||
|
||||
def wrap_certificate(self, cert):
|
||||
if 'body' not in cert:
|
||||
return
|
||||
# get public_key in OpenSSH format
|
||||
public_key = parse_certificate(cert['body']).public_key().public_bytes(
|
||||
encoding=serialization.Encoding.OpenSSH,
|
||||
format=serialization.PublicFormat.OpenSSH,
|
||||
).decode()
|
||||
public_key += ' ' + cert['user']['email']
|
||||
# sign it with authority private key
|
||||
if 'root_authority' in cert and cert['root_authority']:
|
||||
authority = cert['root_authority']
|
||||
else:
|
||||
authority = cert['authority']
|
||||
root_authority = get_by_root_authority(authority['id'])
|
||||
authority_private_key = root_authority.private_key
|
||||
cert['body'] = sign_certificate(
|
||||
cert['common_name'],
|
||||
public_key,
|
||||
authority_private_key,
|
||||
cert['user'],
|
||||
cert['extensions'],
|
||||
cert['not_before'],
|
||||
cert['not_after']
|
||||
)
|
||||
# convert chain in OpenSSH format
|
||||
if cert['chain']:
|
||||
chain_cert = {'body': cert['chain'], 'cn': root_authority.cn}
|
||||
self.wrap_auth_certificate(chain_cert)
|
||||
cert['chain'] = chain_cert['body']
|
||||
# OpenSSH do not support csr
|
||||
cert['csr'] = None
|
||||
|
||||
@staticmethod
|
||||
def wrap_auth_certificate(auth_cert):
|
||||
# convert chain in OpenSSH format
|
||||
chain_key = parse_certificate(auth_cert['body']).public_key().public_bytes(
|
||||
encoding=serialization.Encoding.OpenSSH,
|
||||
format=serialization.PublicFormat.OpenSSH,
|
||||
).decode()
|
||||
chain_key += ' root@' + auth_cert['cn']
|
||||
auth_cert['body'] = split_cert(chain_key)
|
||||
|
||||
@staticmethod
|
||||
def wrap_private_key(cert):
|
||||
# convert private_key in OpenSSH format
|
||||
cert.private_key = parse_private_key(cert.private_key).private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.OpenSSH,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
@ -36,6 +36,7 @@ from .factories import (
|
||||
InvalidCertificateFactory,
|
||||
CryptoAuthorityFactory,
|
||||
CACertificateFactory,
|
||||
DnsProviderFactory,
|
||||
)
|
||||
|
||||
|
||||
@ -183,6 +184,13 @@ def user(session):
|
||||
return {"user": u, "token": token}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dns_provider(session):
|
||||
d = DnsProviderFactory()
|
||||
session.commit()
|
||||
return d
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pending_certificate(session):
|
||||
u = UserFactory()
|
||||
|
@ -1,14 +1,15 @@
|
||||
import json
|
||||
from datetime import date
|
||||
|
||||
from factory import Sequence, post_generation, SubFactory
|
||||
from factory.alchemy import SQLAlchemyModelFactory
|
||||
from factory.fuzzy import FuzzyChoice, FuzzyText, FuzzyDate, FuzzyInteger
|
||||
|
||||
|
||||
from lemur.database import db
|
||||
from lemur.authorities.models import Authority
|
||||
from lemur.certificates.models import Certificate
|
||||
from lemur.destinations.models import Destination
|
||||
from lemur.dns_providers.models import DnsProvider
|
||||
from lemur.sources.models import Source
|
||||
from lemur.notifications.models import Notification
|
||||
from lemur.pending_certificates.models import PendingCertificate
|
||||
@ -435,3 +436,17 @@ class PendingCertificateFactory(BaseFactory):
|
||||
if extracted:
|
||||
for domain in extracted:
|
||||
self.roles.append(domain)
|
||||
|
||||
|
||||
class DnsProviderFactory(BaseFactory):
|
||||
"""DnsProvider Factory."""
|
||||
|
||||
name = Sequence(lambda n: f"dnsProvider{n}")
|
||||
description = FuzzyText(length=128)
|
||||
provider_type = FuzzyText(length=128)
|
||||
credentials = json.dumps({"account_id": f"{FuzzyInteger(100000, 999999).fuzz()}"})
|
||||
|
||||
class Meta:
|
||||
"""Factory Configuration."""
|
||||
|
||||
model = DnsProvider
|
||||
|
@ -1,5 +1,7 @@
|
||||
import json
|
||||
import unittest
|
||||
from lemur.dns_providers import util as dnsutil
|
||||
from lemur.dns_providers.schemas import dns_provider_output_schema
|
||||
|
||||
|
||||
class TestDNSProvider(unittest.TestCase):
|
||||
@ -21,3 +23,17 @@ class TestDNSProvider(unittest.TestCase):
|
||||
self.assertFalse(dnsutil.is_valid_domain('example..io'))
|
||||
self.assertFalse(dnsutil.is_valid_domain('exa mple.io'))
|
||||
self.assertFalse(dnsutil.is_valid_domain('-'))
|
||||
|
||||
|
||||
def test_output_schema(dns_provider):
|
||||
# no credentials using the output schema dump
|
||||
assert dns_provider.credentials
|
||||
assert json.loads(dns_provider.credentials)["account_id"]
|
||||
dump = dns_provider_output_schema.dump(dns_provider).data
|
||||
assert 'name' in dump
|
||||
assert 'credentials' not in dump
|
||||
|
||||
|
||||
def test_json(dns_provider):
|
||||
# we can still get credentials using json.load
|
||||
assert 'account_id' in json.loads(dns_provider.credentials)
|
||||
|
@ -50,7 +50,7 @@ packaging==20.9
|
||||
# via bleach
|
||||
pkginfo==1.5.0.1
|
||||
# via twine
|
||||
pre-commit==2.11.0
|
||||
pre-commit==2.11.1
|
||||
# via -r requirements-dev.in
|
||||
pycodestyle==2.6.0
|
||||
# via flake8
|
||||
|
@ -5,7 +5,10 @@
|
||||
# pip-compile --no-index --output-file=requirements-docs.txt requirements-docs.in
|
||||
#
|
||||
acme==1.13.0
|
||||
# via -r requirements-docs.in
|
||||
# via
|
||||
# -r requirements-docs.in
|
||||
# -r requirements-tests.txt
|
||||
# certbot
|
||||
alabaster==0.7.12
|
||||
# via sphinx
|
||||
alembic==1.5.5
|
||||
@ -48,7 +51,7 @@ blinker==1.4
|
||||
# flask-mail
|
||||
# flask-principal
|
||||
# raven
|
||||
boto3==1.17.22
|
||||
boto3==1.17.27
|
||||
# via
|
||||
# -r requirements-docs.in
|
||||
# -r requirements-tests.txt
|
||||
@ -58,7 +61,7 @@ boto==2.49.0
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# moto
|
||||
botocore==1.20.22
|
||||
botocore==1.20.27
|
||||
# via
|
||||
# -r requirements-docs.in
|
||||
# -r requirements-tests.txt
|
||||
@ -67,6 +70,9 @@ botocore==1.20.22
|
||||
# moto
|
||||
# s3transfer
|
||||
certbot==1.13.0
|
||||
# via
|
||||
# -r requirements-docs.in
|
||||
# -r requirements-tests.txt
|
||||
certifi==2020.12.5
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
@ -94,6 +100,14 @@ click==7.1.2
|
||||
# flask
|
||||
cloudflare==2.8.15
|
||||
# via -r requirements-docs.in
|
||||
configargparse==1.4
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# certbot
|
||||
configobj==5.0.6
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# certbot
|
||||
coverage==5.5
|
||||
# via -r requirements-tests.txt
|
||||
cryptography==3.4.6
|
||||
@ -101,6 +115,7 @@ cryptography==3.4.6
|
||||
# -r requirements-docs.in
|
||||
# -r requirements-tests.txt
|
||||
# acme
|
||||
# certbot
|
||||
# josepy
|
||||
# moto
|
||||
# paramiko
|
||||
@ -111,6 +126,10 @@ decorator==4.4.2
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# networkx
|
||||
distro==1.5.0
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# certbot
|
||||
dnspython3==1.15.0
|
||||
# via -r requirements-docs.in
|
||||
dnspython==1.15.0
|
||||
@ -226,7 +245,9 @@ jmespath==0.9.5
|
||||
josepy==1.7.0
|
||||
# via
|
||||
# -r requirements-docs.in
|
||||
# -r requirements-tests.txt
|
||||
# acme
|
||||
# certbot
|
||||
jsondiff==1.1.2
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
@ -293,6 +314,10 @@ packaging==20.3
|
||||
# sphinx
|
||||
paramiko==2.7.2
|
||||
# via -r requirements-docs.in
|
||||
parsedatetime==2.6
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# certbot
|
||||
pathspec==0.8.0
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
@ -339,6 +364,7 @@ pynacl==1.4.0
|
||||
pyopenssl==20.0.1
|
||||
# via
|
||||
# -r requirements-docs.in
|
||||
# -r requirements-tests.txt
|
||||
# acme
|
||||
# josepy
|
||||
pyparsing==2.4.7
|
||||
@ -346,7 +372,10 @@ pyparsing==2.4.7
|
||||
# -r requirements-tests.txt
|
||||
# packaging
|
||||
pyrfc3339==1.1
|
||||
# via acme
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# acme
|
||||
# certbot
|
||||
pyrsistent==0.16.0
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
@ -382,6 +411,7 @@ pytz==2019.3
|
||||
# -r requirements-tests.txt
|
||||
# acme
|
||||
# babel
|
||||
# certbot
|
||||
# flask-restful
|
||||
# moto
|
||||
# pyrfc3339
|
||||
@ -406,7 +436,9 @@ regex==2020.4.4
|
||||
requests-mock==1.8.0
|
||||
# via -r requirements-tests.txt
|
||||
requests-toolbelt==0.9.1
|
||||
# via acme
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# acme
|
||||
requests==2.25.1
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
@ -441,6 +473,7 @@ six==1.15.0
|
||||
# bandit
|
||||
# bcrypt
|
||||
# cfn-lint
|
||||
# configobj
|
||||
# docker
|
||||
# ecdsa
|
||||
# fakeredis
|
||||
@ -564,6 +597,36 @@ zipp==3.1.0
|
||||
# -r requirements-tests.txt
|
||||
# importlib-metadata
|
||||
# moto
|
||||
zope.component==4.6.2
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# certbot
|
||||
zope.deferredimport==4.3.1
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# zope.component
|
||||
zope.deprecation==4.4.0
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# zope.component
|
||||
zope.event==4.5.0
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# zope.component
|
||||
zope.hookable==5.0.1
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# zope.component
|
||||
zope.interface==5.2.0
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# certbot
|
||||
# zope.component
|
||||
# zope.proxy
|
||||
zope.proxy==4.3.5
|
||||
# via
|
||||
# -r requirements-tests.txt
|
||||
# zope.deferredimport
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
|
@ -4,6 +4,8 @@
|
||||
#
|
||||
# pip-compile --no-index --output-file=requirements-tests.txt requirements-tests.in
|
||||
#
|
||||
acme==1.13.0
|
||||
# via certbot
|
||||
appdirs==1.4.3
|
||||
# via black
|
||||
attrs==19.3.0
|
||||
@ -18,19 +20,20 @@ bandit==1.7.0
|
||||
# via -r requirements-tests.in
|
||||
black==20.8b1
|
||||
# via -r requirements-tests.in
|
||||
boto3==1.17.22
|
||||
boto3==1.17.27
|
||||
# via
|
||||
# aws-sam-translator
|
||||
# moto
|
||||
boto==2.49.0
|
||||
# via moto
|
||||
botocore==1.20.22
|
||||
botocore==1.20.27
|
||||
# via
|
||||
# aws-xray-sdk
|
||||
# boto3
|
||||
# moto
|
||||
# s3transfer
|
||||
certbot==1.13.0
|
||||
# via -r requirements-tests.in
|
||||
certifi==2020.12.5
|
||||
# via requests
|
||||
cffi==1.14.0
|
||||
@ -43,15 +46,25 @@ click==7.1.2
|
||||
# via
|
||||
# black
|
||||
# flask
|
||||
configargparse==1.4
|
||||
# via certbot
|
||||
configobj==5.0.6
|
||||
# via certbot
|
||||
coverage==5.5
|
||||
# via -r requirements-tests.in
|
||||
cryptography==3.4.6
|
||||
# via
|
||||
# acme
|
||||
# certbot
|
||||
# josepy
|
||||
# moto
|
||||
# pyopenssl
|
||||
# python-jose
|
||||
# sshpubkeys
|
||||
decorator==4.4.2
|
||||
# via networkx
|
||||
distro==1.5.0
|
||||
# via certbot
|
||||
docker==4.2.0
|
||||
# via moto
|
||||
ecdsa==0.14.1
|
||||
@ -95,6 +108,10 @@ jmespath==0.9.5
|
||||
# via
|
||||
# boto3
|
||||
# botocore
|
||||
josepy==1.7.0
|
||||
# via
|
||||
# acme
|
||||
# certbot
|
||||
jsondiff==1.1.2
|
||||
# via moto
|
||||
jsonpatch==1.25
|
||||
@ -125,6 +142,8 @@ nose==1.3.7
|
||||
# via -r requirements-tests.in
|
||||
packaging==20.3
|
||||
# via pytest
|
||||
parsedatetime==2.6
|
||||
# via certbot
|
||||
pathspec==0.8.0
|
||||
# via black
|
||||
pbr==5.4.5
|
||||
@ -141,8 +160,16 @@ pycparser==2.20
|
||||
# via cffi
|
||||
pyflakes==2.2.0
|
||||
# via -r requirements-tests.in
|
||||
pyopenssl==20.0.1
|
||||
# via
|
||||
# acme
|
||||
# josepy
|
||||
pyparsing==2.4.7
|
||||
# via packaging
|
||||
pyrfc3339==1.1
|
||||
# via
|
||||
# acme
|
||||
# certbot
|
||||
pyrsistent==0.16.0
|
||||
# via jsonschema
|
||||
pytest-flask==1.2.0
|
||||
@ -163,7 +190,11 @@ python-dateutil==2.8.1
|
||||
python-jose[cryptography]==3.1.0
|
||||
# via moto
|
||||
pytz==2019.3
|
||||
# via moto
|
||||
# via
|
||||
# acme
|
||||
# certbot
|
||||
# moto
|
||||
# pyrfc3339
|
||||
pyyaml==5.4.1
|
||||
# via
|
||||
# -r requirements-tests.in
|
||||
@ -176,11 +207,15 @@ regex==2020.4.4
|
||||
# via black
|
||||
requests-mock==1.8.0
|
||||
# via -r requirements-tests.in
|
||||
requests-toolbelt==0.9.1
|
||||
# via acme
|
||||
requests==2.25.1
|
||||
# via
|
||||
# acme
|
||||
# docker
|
||||
# moto
|
||||
# requests-mock
|
||||
# requests-toolbelt
|
||||
# responses
|
||||
responses==0.10.12
|
||||
# via moto
|
||||
@ -193,12 +228,15 @@ six==1.15.0
|
||||
# aws-sam-translator
|
||||
# bandit
|
||||
# cfn-lint
|
||||
# configobj
|
||||
# docker
|
||||
# ecdsa
|
||||
# fakeredis
|
||||
# josepy
|
||||
# jsonschema
|
||||
# moto
|
||||
# packaging
|
||||
# pyopenssl
|
||||
# pyrsistent
|
||||
# python-dateutil
|
||||
# python-jose
|
||||
@ -243,6 +281,23 @@ zipp==3.1.0
|
||||
# via
|
||||
# importlib-metadata
|
||||
# moto
|
||||
zope.component==4.6.2
|
||||
# via certbot
|
||||
zope.deferredimport==4.3.1
|
||||
# via zope.component
|
||||
zope.deprecation==4.4.0
|
||||
# via zope.component
|
||||
zope.event==4.5.0
|
||||
# via zope.component
|
||||
zope.hookable==5.0.1
|
||||
# via zope.component
|
||||
zope.interface==5.2.0
|
||||
# via
|
||||
# certbot
|
||||
# zope.component
|
||||
# zope.proxy
|
||||
zope.proxy==4.3.5
|
||||
# via zope.deferredimport
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
|
@ -5,7 +5,9 @@
|
||||
# pip-compile --no-index --output-file=requirements.txt requirements.in
|
||||
#
|
||||
acme==1.13.0
|
||||
# via -r requirements.in
|
||||
# via
|
||||
# -r requirements.in
|
||||
# certbot
|
||||
alembic-autogenerate-enums==0.0.2
|
||||
# via -r requirements.in
|
||||
alembic==1.4.2
|
||||
@ -31,9 +33,9 @@ blinker==1.4
|
||||
# flask-mail
|
||||
# flask-principal
|
||||
# raven
|
||||
boto3==1.17.22
|
||||
boto3==1.17.27
|
||||
# via -r requirements.in
|
||||
botocore==1.20.22
|
||||
botocore==1.20.27
|
||||
# via
|
||||
# -r requirements.in
|
||||
# boto3
|
||||
@ -41,6 +43,7 @@ botocore==1.20.22
|
||||
celery[redis]==4.4.2
|
||||
# via -r requirements.in
|
||||
certbot==1.13.0
|
||||
# via -r requirements.in
|
||||
certifi==2020.12.5
|
||||
# via
|
||||
# -r requirements.in
|
||||
@ -58,13 +61,20 @@ click==7.1.2
|
||||
# via flask
|
||||
cloudflare==2.8.15
|
||||
# via -r requirements.in
|
||||
configargparse==1.4
|
||||
# via certbot
|
||||
configobj==5.0.6
|
||||
# via certbot
|
||||
cryptography==3.4.6
|
||||
# via
|
||||
# -r requirements.in
|
||||
# acme
|
||||
# certbot
|
||||
# josepy
|
||||
# paramiko
|
||||
# pyopenssl
|
||||
distro==1.5.0
|
||||
# via certbot
|
||||
dnspython3==1.15.0
|
||||
# via -r requirements.in
|
||||
dnspython==1.15.0
|
||||
@ -126,7 +136,9 @@ jmespath==0.9.5
|
||||
# boto3
|
||||
# botocore
|
||||
josepy==1.7.0
|
||||
# via acme
|
||||
# via
|
||||
# acme
|
||||
# certbot
|
||||
jsonlines==1.2.0
|
||||
# via cloudflare
|
||||
kombu==4.6.8
|
||||
@ -151,6 +163,8 @@ ndg-httpsclient==0.5.1
|
||||
# via -r requirements.in
|
||||
paramiko==2.7.2
|
||||
# via -r requirements.in
|
||||
parsedatetime==2.6
|
||||
# via certbot
|
||||
pem==21.1.0
|
||||
# via -r requirements.in
|
||||
psycopg2==2.8.6
|
||||
@ -182,7 +196,9 @@ pyopenssl==20.0.1
|
||||
# josepy
|
||||
# ndg-httpsclient
|
||||
pyrfc3339==1.1
|
||||
# via acme
|
||||
# via
|
||||
# acme
|
||||
# certbot
|
||||
python-dateutil==2.8.1
|
||||
# via
|
||||
# alembic
|
||||
@ -198,6 +214,7 @@ pytz==2019.3
|
||||
# via
|
||||
# acme
|
||||
# celery
|
||||
# certbot
|
||||
# flask-restful
|
||||
# pyrfc3339
|
||||
pyyaml==5.4.1
|
||||
@ -228,6 +245,7 @@ six==1.15.0
|
||||
# via
|
||||
# -r requirements.in
|
||||
# bcrypt
|
||||
# configobj
|
||||
# flask-cors
|
||||
# flask-restful
|
||||
# hvac
|
||||
@ -264,6 +282,22 @@ werkzeug==1.0.1
|
||||
# via flask
|
||||
xmltodict==0.12.0
|
||||
# via -r requirements.in
|
||||
zope.component==4.6.2
|
||||
# via certbot
|
||||
zope.deferredimport==4.3.1
|
||||
# via zope.component
|
||||
zope.deprecation==4.4.0
|
||||
# via zope.component
|
||||
zope.event==4.5.0
|
||||
# via zope.component
|
||||
zope.hookable==5.0.1
|
||||
# via zope.component
|
||||
zope.interface==5.2.0
|
||||
# via
|
||||
# certbot
|
||||
# zope.component
|
||||
zope.proxy==4.3.5
|
||||
# via zope.deferredimport
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
|
1
setup.py
1
setup.py
@ -158,6 +158,7 @@ setup(
|
||||
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin',
|
||||
'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin',
|
||||
'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin',
|
||||
'openssh_issuer = lemur.plugins.lemur_openssh.plugin:OpenSSHIssuerPlugin',
|
||||
'azure_destination = lemur.plugins.lemur_azure_dest.plugin:AzureDestinationPlugin'
|
||||
],
|
||||
},
|
||||
|
Reference in New Issue
Block a user