Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS ACM Private CA support #256

Merged
merged 33 commits into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
43fb7f8
Basic proof of concept of using AWS ACM Private CA
Jan 28, 2020
829df19
Fixes
Jan 28, 2020
dc5b1e5
Lint fix
Jan 29, 2020
e847aaf
Code docs and basic fixes
Jan 29, 2020
80b91a4
Lint fix
Jan 29, 2020
20dee64
Fixes for issue certificate
Jan 29, 2020
ba61405
Add tests for certificatemanager
Jan 30, 2020
ee271ba
Route tests and fixes for csr post route
Jan 30, 2020
c82267a
Add in-process caching for generating cert/key/csr
Feb 6, 2020
afa646a
Fix route test
Feb 6, 2020
7f3ce7a
Support multiple CAs in the same instance
Feb 10, 2020
1037489
Settings updates
Feb 11, 2020
fd44418
Fixes for PR feedback
Feb 12, 2020
b75ada5
No more service-specific checks
Feb 13, 2020
2026a82
Use ACLs for certificate routes, and support in default_acl
Feb 13, 2020
c52dde0
No default for name regex
Feb 13, 2020
69e4c5b
Add stats for certificate service
Feb 13, 2020
ea60471
Fix doc
Feb 13, 2020
6ba071b
More doc fix
Feb 13, 2020
3a6e2c4
Add configuration docs for CA
Feb 14, 2020
a41250f
Add get and list endpoints for fetching CAs
Feb 18, 2020
c676d53
Fix get_certificate_authority_certificate test
Feb 18, 2020
db08fb0
Str not str in schema
Feb 18, 2020
1a162ac
Add ca routes to routes tests
Feb 18, 2020
740ab74
Test fixes
Feb 19, 2020
4f15892
More fixes
Feb 19, 2020
a343f82
Remove unreferenced variable
Feb 19, 2020
35838b5
Fix schema and routes and tests
Feb 19, 2020
99533a0
Decode private key, so that we do not output a binary string in json …
Feb 19, 2020
331359c
Consistent use of string vs bytes
Feb 19, 2020
b55f724
Doc fixes
Feb 19, 2020
c0b0110
Add a no-op cache class so nothing is cached
Feb 24, 2020
c1dd56c
Ratelimit locked cached certificate responses
Feb 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions confidant/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from confidant import settings
from confidant.routes import (
blind_credentials,
certificates,
credentials,
identity,
saml,
Expand Down Expand Up @@ -54,6 +55,7 @@ def create_app():

app.register_blueprint(blind_credentials.blueprint)
app.register_blueprint(credentials.blueprint)
app.register_blueprint(certificates.blueprint)
app.register_blueprint(identity.blueprint)
app.register_blueprint(saml.blueprint)
app.register_blueprint(services.blueprint)
Expand Down
48 changes: 42 additions & 6 deletions confidant/authnz/rbac.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,63 @@
import re

from confidant import authnz
from confidant.services import certificatemanager


def default_acl(*args, **kwargs):
""" Default ACLs for confidant: always return true for users, but enforce
ACLs for services, restricting access to:
""" Default ACLs for confidant: Allow access to all resource types
and actions for users, except for certificate resource_type. Deny access
to all resource types and actions for services, except:

* resource_type: service
actions: metadata, get
resource_id: must match logged-in user's username
* resource_type: certificate
actions: get
resource_id: must match against ACM_PRIVATE_CA_DOMAIN_REGEX setting
for the CA for the CN in the CSR, and for all SAN values in the CSR,
and the server_name named group in the regex must match the logged
in user's username.
kwargs (ca): CA used for this get
kwargs (san): A list of subject alternative names in the CSR
"""
resource_type = kwargs.get('resource_type')
action = kwargs.get('action')
resource_id = kwargs.get('resource_id')
# Some ACL checks also pass extra args in via kwargs, which we would
# access via:
# resource_kwargs = kwargs.get('kwargs')
resource_kwargs = kwargs.get('kwargs')
if authnz.user_is_user_type('user'):
if resource_type == 'certificate':
return False
elif resource_type == 'ca':
return False
return True
elif authnz.user_is_user_type('service'):
if resource_type == 'service' and action in ['metadata', 'get']:
# Does the resource ID match the authenticated username?
if authnz.user_is_service(resource_id):
return True
# We currently only allow services to access service get/metadata
elif resource_type == 'ca' and action in ['list', 'get']:
return True
elif resource_type == 'certificate' and action in ['get']:
ca_object = certificatemanager.get_ca(resource_kwargs.get('ca'))
# Require a name pattern
if not ca_object.settings['name_regex']:
return False
cert_pattern = re.compile(ca_object.settings['name_regex'])
domains = [resource_id]
domains.extend(resource_kwargs.get('san', []))
# Ensure the CN and every value in the SAN is allowed for this
# user.
for domain in domains:
match = cert_pattern.match(domain)
if not match:
return False
service_name = match.group('service_name')
if not service_name:
return False
if not authnz.user_is_service(service_name):
return False
skiptomyliu marked this conversation as resolved.
Show resolved Hide resolved
return True
return False
else:
# This should never happen, but paranoia wins out
Expand Down
213 changes: 213 additions & 0 deletions confidant/routes/certificates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import logging

from flask import blueprints, jsonify, request

from confidant import authnz, settings
from confidant.services import certificatemanager
from confidant.schema.certificates import (
certificate_authority_response_schema,
certificate_authorities_response_schema,
certificate_expanded_response_schema,
certificate_response_schema,
CertificateAuthorityResponse,
CertificateAuthoritiesResponse,
CertificateResponse,
)
from confidant.utils import misc

blueprint = blueprints.Blueprint('certificates', __name__)

acl_module_check = misc.load_module(settings.ACL_MODULE)


@blueprint.route('/v1/certificates/<ca>/<cn>', methods=['GET'])
@authnz.require_auth
def get_certificate(ca, cn):
'''
Get a certificate for the provided cn, using the provided CA.
'''
try:
ca_object = certificatemanager.get_ca(ca)
except certificatemanager.CertificateAuthorityNotFoundError:
return jsonify({'error': 'Provided CA not found.'}), 404
san = request.args.getlist('san')

logged_in_user = authnz.get_logged_in_user()
if not acl_module_check(
resource_type='certificate',
action='get',
resource_id=cn,
kwargs={
'ca': ca,
'san': san,
},
):
msg = ('{} does not have access to get certificate cn {} against'
' ca {}').format(
authnz.get_logged_in_user(),
cn,
ca,
)
error_msg = {'error': msg, 'reference': cn}
return jsonify(error_msg), 403

logging.info(
'get_certificate called on id={} for ca={} by user={}'.format(
cn,
ca,
logged_in_user,
)
)

validity = request.args.get(
'validity',
default=ca_object.settings['max_validity_days'],
type=int,
)
certificate = ca_object.issue_certificate_with_key(
cn,
validity,
san,
)
certificate_response = CertificateResponse(
certificate=certificate['certificate'],
certificate_chain=certificate['certificate_chain'],
key=certificate['key'],
)
return certificate_expanded_response_schema.dumps(certificate_response)


@blueprint.route('/v1/certificates/<ca>', methods=['POST'])
@authnz.require_auth
@authnz.require_csrf_token
def get_certificate_from_csr(ca):
'''
Get a certificate from the ca provided in the url, using the CSR, validity
and san provided in the POST body.
'''
try:
ca_object = certificatemanager.get_ca(ca)
except certificatemanager.CertificateAuthorityNotFoundError:
return jsonify({'error': 'Provided CA not found.'}), 404
data = request.get_json()
if not data or not data.get('csr'):
return jsonify(
{'error': 'csr must be provided in the POST body.'},
), 400
validity = data.get(
'validity',
ca_object.settings['max_validity_days'],
)
try:
csr = ca_object.decode_csr(data['csr'])
except Exception:
logging.exception('Failed to decode PEM csr')
return jsonify(
{'error': 'csr could not be decoded'},
), 400
# Get the cn and san values from the csr object, so that we can use them
# for the ACL check.
cn = ca_object.get_csr_common_name(csr)
san = ca_object.get_csr_san(csr)

logged_in_user = authnz.get_logged_in_user()
if not acl_module_check(
resource_type='certificate',
action='get',
resource_id=cn,
kwargs={
'ca': ca,
'san': san,
},
):
msg = ('{} does not have access to get certificate cn {} against'
' ca {}').format(
authnz.get_logged_in_user(),
cn,
ca,
)
error_msg = {'error': msg, 'reference': cn}
return jsonify(error_msg), 403

logging.info(
'get_certificate called on id={} for ca={} by user={}'.format(
cn,
ca,
logged_in_user,
)
)

arn = ca_object.issue_certificate(data['csr'], validity)
certificate = ca_object.get_certificate_from_arn(arn)
certificate_response = CertificateResponse(
certificate=certificate['certificate'],
certificate_chain=certificate['certificate_chain'],
)
return certificate_response_schema.dumps(certificate_response)


@blueprint.route('/v1/cas', methods=['GET'])
@authnz.require_auth
def list_cas():
'''
List the configured CAs.
'''

logged_in_user = authnz.get_logged_in_user()
if not acl_module_check(
resource_type='ca',
action='list',
):
msg = '{} does not have access to list cas'.format(
authnz.get_logged_in_user(),
)
error_msg = {'error': msg}
return jsonify(error_msg), 403

cas = certificatemanager.list_cas()

logging.info('list_cas called by user={}'.format(logged_in_user))

cas_response = CertificateAuthoritiesResponse.from_cas(cas)
return certificate_authorities_response_schema.dumps(cas_response)


@blueprint.route('/v1/cas/<ca>', methods=['GET'])
@authnz.require_auth
def get_ca(ca):
'''
Get the CA information for the provided ca.
'''
try:
ca_object = certificatemanager.get_ca(ca)
except certificatemanager.CertificateAuthorityNotFoundError:
return jsonify({'error': 'Provided CA not found.'}), 404

logged_in_user = authnz.get_logged_in_user()
if not acl_module_check(
resource_type='ca',
action='get',
resource_id=ca,
):
msg = '{} does not have access to get ca {}'.format(
authnz.get_logged_in_user(),
ca,
)
error_msg = {'error': msg, 'reference': ca}
return jsonify(error_msg), 403

logging.info(
'get_ca called on id={} by user={}'.format(
ca,
logged_in_user,
)
)

_ca = ca_object.get_certificate_authority_certificate()
ca_response = CertificateAuthorityResponse(
ca=_ca['ca'],
certificate=_ca['certificate'],
certificate_chain=_ca['certificate_chain'],
tags=_ca['tags'],
)
return certificate_authority_response_schema.dumps(ca_response)
86 changes: 86 additions & 0 deletions confidant/schema/certificates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import attr
import toastedmarshmallow
from marshmallow import fields

from confidant.schema.auto_build_schema import AutobuildSchema


@attr.s
class CertificateAuthorityResponse(object):
ca = attr.ib()
certificate = attr.ib()
certificate_chain = attr.ib()
tags = attr.ib()


@attr.s
class CertificateAuthoritiesResponse(object):
cas = attr.ib()

@classmethod
def from_cas(cls, cas):
return cls(
cas=[
CertificateAuthorityResponse(
ca=ca['ca'],
certificate=ca['certificate'],
certificate_chain=ca['certificate_chain'],
tags=ca['tags'])
for ca in cas
],
)


class CertificateAuthorityResponseSchema(AutobuildSchema):
class Meta:
jit = toastedmarshmallow.Jit

_class_to_load = CertificateAuthorityResponse

ca = fields.Str(required=True)
certificate = fields.Str(required=True)
certificate_chain = fields.Str(required=True)
tags = fields.Dict(keys=fields.Str(), values=fields.Str())


class CertificateAuthoritiesResponseSchema(AutobuildSchema):
class Meta:
jit = toastedmarshmallow.Jit

_class_to_load = CertificateAuthoritiesResponse

cas = fields.Nested(CertificateAuthorityResponseSchema, many=True)


@attr.s
class CertificateResponse(object):
certificate = attr.ib()
certificate_chain = attr.ib()
key = attr.ib(default=None)


class CertificateResponseSchema(AutobuildSchema):
class Meta:
jit = toastedmarshmallow.Jit

_class_to_load = CertificateResponse

certificate = fields.Str(required=True)
certificate_chain = fields.Str(required=True)


class CertificateExpandedResponseSchema(AutobuildSchema):
class Meta:
jit = toastedmarshmallow.Jit

_class_to_load = CertificateResponse

certificate = fields.Str(required=True)
certificate_chain = fields.Str(required=True)
key = fields.Str()


certificate_response_schema = CertificateResponseSchema()
certificate_authority_response_schema = CertificateAuthorityResponseSchema()
certificate_authorities_response_schema = CertificateAuthoritiesResponseSchema()
certificate_expanded_response_schema = CertificateExpandedResponseSchema()
Loading