Compare commits
No commits in common. "b09601838de127f55fd5e9227cf12cef8993f004" and "435708f98699810f29553fd6d0769c7908cc00d9" have entirely different histories.
b09601838d
...
435708f986
|
@ -599,119 +599,4 @@ 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>"}
|
{"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.
|
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 flask import current_app
|
||||||
|
|
||||||
from marshmallow import fields, validates_schema, pre_load, post_dump
|
from marshmallow import fields, validates_schema, pre_load
|
||||||
from marshmallow import validate
|
from marshmallow import validate
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ from lemur.common import validators, missing
|
||||||
|
|
||||||
from lemur.common.fields import ArrowDateTime
|
from lemur.common.fields import ArrowDateTime
|
||||||
from lemur.constants import CERTIFICATE_KEY_TYPES
|
from lemur.constants import CERTIFICATE_KEY_TYPES
|
||||||
from lemur.plugins.base import plugins
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorityInputSchema(LemurInputSchema):
|
class AuthorityInputSchema(LemurInputSchema):
|
||||||
|
@ -130,12 +129,6 @@ class AuthorityOutputSchema(LemurOutputSchema):
|
||||||
default_validity_days = fields.Integer()
|
default_validity_days = fields.Integer()
|
||||||
authority_certificate = fields.Nested(RootAuthorityCertificateOutputSchema)
|
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):
|
class AuthorityNestedOutputSchema(LemurOutputSchema):
|
||||||
__envelope__ = False
|
__envelope__ = False
|
||||||
|
|
|
@ -38,7 +38,6 @@ from lemur.schemas import (
|
||||||
AssociatedRotationPolicySchema,
|
AssociatedRotationPolicySchema,
|
||||||
)
|
)
|
||||||
from lemur.users.schemas import UserNestedOutputSchema
|
from lemur.users.schemas import UserNestedOutputSchema
|
||||||
from lemur.plugins.base import plugins
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSchema(LemurInputSchema):
|
class CertificateSchema(LemurInputSchema):
|
||||||
|
@ -325,8 +324,6 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||||
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
|
||||||
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
|
||||||
authority = fields.Nested(AuthorityNestedOutputSchema)
|
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)
|
dns_provider = fields.Nested(DnsProvidersNestedOutputSchema)
|
||||||
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
roles = fields.Nested(RoleNestedOutputSchema, many=True)
|
||||||
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
|
||||||
|
@ -360,21 +357,6 @@ class CertificateOutputSchema(LemurOutputSchema):
|
||||||
if field in data and data[field] is None:
|
if field in data and data[field] is None:
|
||||||
data.pop(field)
|
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)
|
|
||||||
del cert['root_authority']
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateShortOutputSchema(LemurOutputSchema):
|
class CertificateShortOutputSchema(LemurOutputSchema):
|
||||||
id = fields.Integer()
|
id = fields.Integer()
|
||||||
|
|
|
@ -88,16 +88,6 @@ def get_by_attributes(conditions):
|
||||||
return database.find_all(query, Certificate, conditions).all()
|
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):
|
def delete(cert_id):
|
||||||
"""
|
"""
|
||||||
Delete's a certificate.
|
Delete's a certificate.
|
||||||
|
|
|
@ -33,7 +33,6 @@ from lemur.certificates.schemas import (
|
||||||
|
|
||||||
from lemur.roles import service as role_service
|
from lemur.roles import service as role_service
|
||||||
from lemur.logs import service as log_service
|
from lemur.logs import service as log_service
|
||||||
from lemur.plugins.base import plugins
|
|
||||||
|
|
||||||
|
|
||||||
mod = Blueprint("certificates", __name__)
|
mod = Blueprint("certificates", __name__)
|
||||||
|
@ -692,16 +691,6 @@ class CertificatePrivateKey(AuthenticatedResource):
|
||||||
return dict(message="You are not authorized to view this key"), 403
|
return dict(message="You are not authorized to view this key"), 403
|
||||||
|
|
||||||
log_service.create(g.current_user, "key_view", certificate=cert)
|
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 = make_response(jsonify(key=cert.private_key), 200)
|
||||||
response.headers["cache-control"] = "private, max-age=0, no-cache, no-store"
|
response.headers["cache-control"] = "private, max-age=0, no-cache, no-store"
|
||||||
response.headers["pragma"] = "no-cache"
|
response.headers["pragma"] = "no-cache"
|
||||||
|
|
|
@ -31,12 +31,3 @@ class IssuerPlugin(Plugin):
|
||||||
|
|
||||||
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
def cancel_ordered_certificate(self, pending_cert, **kwargs):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def wrap_certificate(self, cert):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def wrap_auth_certificate(self, cert):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def wrap_private_key(self, cert):
|
|
||||||
pass
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
try:
|
|
||||||
VERSION = __import__("pkg_resources").get_distribution(__name__).version
|
|
||||||
except Exception as e:
|
|
||||||
VERSION = "unknown"
|
|
|
@ -1,151 +0,0 @@
|
||||||
"""
|
|
||||||
.. 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):
|
|
||||||
with mktempfile() as issuer_tmp:
|
|
||||||
cmd = ['ssh-keygen', '-s', issuer_tmp]
|
|
||||||
with open(issuer_tmp, 'w') as i:
|
|
||||||
i.writelines(authority_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 20201024
|
|
||||||
ssh_not_before = datetime.fromisoformat(not_before).strftime("%Y%m%d")
|
|
||||||
ssh_not_after = datetime.fromisoformat(not_after).strftime("%Y%m%d")
|
|
||||||
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):
|
|
||||||
# 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:
|
|
||||||
root_authority = cert['root_authority']
|
|
||||||
else:
|
|
||||||
root_authority = get_by_root_authority(cert['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(),
|
|
||||||
)
|
|
1
setup.py
1
setup.py
|
@ -158,7 +158,6 @@ setup(
|
||||||
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin',
|
'adcs_source = lemur.plugins.lemur_adcs.plugin:ADCSSourcePlugin',
|
||||||
'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin',
|
'entrust_issuer = lemur.plugins.lemur_entrust.plugin:EntrustIssuerPlugin',
|
||||||
'entrust_source = lemur.plugins.lemur_entrust.plugin:EntrustSourcePlugin',
|
'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'
|
'azure_destination = lemur.plugins.lemur_azure_dest.plugin:AzureDestinationPlugin'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue