From fa70477628f77eaa4598118c74494be09444aaf1 Mon Sep 17 00:00:00 2001 From: Pino de Candia <32303022+pinodeca@users.noreply.github.com> Date: Thu, 7 Dec 2017 13:54:19 -0600 Subject: [PATCH] Started integrating Castellan --- files/tatu.service | 14 ++++++ tatu/api/models.py | 4 +- tatu/castellan/__init__.py | 0 tatu/castellan/config.py | 52 +++++++++++++++++++++++ tatu/castellan/tatu_key_manager.py | 68 ++++++++++++++++++++++++++++++ tatu/castellan/utils.py | 52 +++++++++++++++++++++++ tatu/db/models.py | 25 ++++++++--- 7 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 files/tatu.service create mode 100644 tatu/castellan/__init__.py create mode 100644 tatu/castellan/config.py create mode 100644 tatu/castellan/tatu_key_manager.py create mode 100644 tatu/castellan/utils.py diff --git a/files/tatu.service b/files/tatu.service new file mode 100644 index 0000000..95d64e5 --- /dev/null +++ b/files/tatu.service @@ -0,0 +1,14 @@ +[Unit] +Description=OpenStack SSH certificates and bastions +After=syslog.target network.target + +[Service] +Type=simple +NotifyAccess=all +TimeoutStartSec=0 +Restart=always +User=tatu +ExecStart=/usr/bin/tatu + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/tatu/api/models.py b/tatu/api/models.py index 24b11e8..5bc05c5 100644 --- a/tatu/api/models.py +++ b/tatu/api/models.py @@ -69,9 +69,9 @@ class Authority(object): if auth is None: resp.status = falcon.HTTP_NOT_FOUND return - user_key = RSA.importKey(auth.user_key) + user_key = RSA.importKey(db.getAuthUserKey(auth)) user_pub_key = user_key.publickey().exportKey('OpenSSH') - host_key = RSA.importKey(auth.host_key) + host_key = RSA.importKey(db.getAuthHostKey(auth)) host_pub_key = host_key.publickey().exportKey('OpenSSH') body = { 'auth_id': auth_id, diff --git a/tatu/castellan/__init__.py b/tatu/castellan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tatu/castellan/config.py b/tatu/castellan/config.py new file mode 100644 index 0000000..1576b57 --- /dev/null +++ b/tatu/castellan/config.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from tatu.castellan import options as castellan +from oslo_config import cfg + +#from sahara.utils.openstack import base as utils + + +opts = [ + cfg.BoolOpt('use_barbican_key_manager', default=False, + help='Enable the usage of the OpenStack Key Management ' + 'service provided by barbican.'), +] + +castellan_opts = [ + cfg.StrOpt('barbican_api_endpoint', + help='The endpoint to use for connecting to the barbican ' + 'api controller. By default, castellan will use the ' + 'URL from the service catalog.'), + cfg.StrOpt('barbican_api_version', default='v1', + help='Version of the barbican API, for example: "v1"'), +] + +castellan_group = cfg.OptGroup(name='castellan', + title='castellan key manager options') + +CONF = cfg.CONF +CONF.register_group(castellan_group) +CONF.register_opts(opts) +CONF.register_opts(castellan_opts, group=castellan_group) + + +def validate_config(): + if CONF.use_barbican_key_manager: + # NOTE (elmiko) there is no need to set the api_class as castellan + # uses barbican by default. + #castellan.set_defaults(CONF, auth_endpoint=utils.retrieve_auth_url()) + castellan.set_defaults(CONF) + else: + castellan.set_defaults(CONF, api_class='tatu.castellan.' + 'tatu_key_manager.TatuKeyManager') \ No newline at end of file diff --git a/tatu/castellan/tatu_key_manager.py b/tatu/castellan/tatu_key_manager.py new file mode 100644 index 0000000..488c366 --- /dev/null +++ b/tatu/castellan/tatu_key_manager.py @@ -0,0 +1,68 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from castellan.common.objects import passphrase as key +from castellan.key_manager import key_manager as km + + +"""tatu.castellan.tatu_key_manager +This module contains the KeyManager class that will be used by the +castellan library, it is not meant for direct usage within tatu. +""" + + +class TatuKeyManager(km.KeyManager): + """Tatu specific key manager + This manager is a thin wrapper around the secret being stored. It is + intended for backward compatible use only. It will not store keys + or generate UUIDs but instead return the secret that is being stored. + This behavior allows Tatu to continue storing secrets in its database + while using the Castellan key manager abstraction. + """ + def __init__(self, configuration=None): + pass + + def create_key(self, context, algorithm=None, length=0, + expiration=None, **kwargs): + """creates a key + algorithm, length, and expiration are unused by tatu keys. + """ + return key.Passphrase(passphrase=kwargs.get('passphrase', '')) + + def create_key_pair(self, *args, **kwargs): + pass + + def store(self, context, key, expiration=None, **kwargs): + """store a key + in normal usage a store_key will return the UUID of the key as + dictated by the key manager. Tatu would then store this UUID in + its database to use for retrieval. As tatu is not actually using + a key manager in this context it will return the key's payload for + storage. + """ + return key.get_encoded() + + def get(self, context, key_id, **kwargs): + """get a key + since tatu is not actually storing key UUIDs the key_id to this + function should actually be the key payload. this function will + simply return a new TatuKey based on that value. + """ + return key.Passphrase(passphrase=key_id) + + def delete(self, context, key_id, **kwargs): + """delete a key + as there is no external key manager, this function will not + perform any external actions. therefore, it won't change anything. + """ + pass \ No newline at end of file diff --git a/tatu/castellan/utils.py b/tatu/castellan/utils.py new file mode 100644 index 0000000..9799ff8 --- /dev/null +++ b/tatu/castellan/utils.py @@ -0,0 +1,52 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from castellan.common.objects import passphrase +from castellan import key_manager + +from oslo_context import context + + +def delete_secret(id, ctx=None): + """delete a secret from the external key manager + :param id: The identifier of the secret to delete + :param ctx: The context, and associated authentication, to use with + this operation (defaults to the current context) + """ + if ctx is None: + ctx = context.current() + key_manager.API().delete(ctx, id) + + +def get_secret(id, ctx=None): + """get a secret associated with an id + :param id: The identifier of the secret to retrieve + :param ctx: The context, and associated authentication, to use with + this operation (defaults to the current context) + """ + if ctx is None: + ctx = context.current() + key = key_manager.API().get(ctx, id) + return key.get_encoded() + + +def store_secret(secret, ctx=None): + """store a secret and return its identifier + :param secret: The secret to store, this should be a string + :param ctx: The context, and associated authentication, to use with + this operation (defaults to the current context) + """ + if ctx is None: + ctx = context.current() + key = passphrase.Passphrase(secret) + return key_manager.API().store(ctx, key) \ No newline at end of file diff --git a/tatu/db/models.py b/tatu/db/models.py index 12d5f1c..b079412 100644 --- a/tatu/db/models.py +++ b/tatu/db/models.py @@ -1,12 +1,13 @@ from datetime import datetime +import falcon +import os import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.exc import IntegrityError -import falcon import sshpubkeys -import uuid -import os +from tatu.castellan.utils import get_secret, store_secret from tatu.utils import generateCert,random_uuid +import uuid from Crypto.PublicKey import RSA Base = declarative_base() @@ -21,10 +22,20 @@ class Authority(Base): def getAuthority(session, auth_id): return session.query(Authority).get(auth_id) +def getAuthUserKey(auth): + return get_secret(auth.user_key) + +def getAuthHostKey(auth): + return get_secret(auth.host_key) + def createAuthority(session, auth_id): + user_key = RSA.generate(2048).exportKey('PEM') + user_secret_id = store_secret(user_key) + host_key = RSA.generate(2048).exportKey('PEM') + host_secret_id = store_secret(host_key) auth = Authority(auth_id=auth_id, - user_key=RSA.generate(2048).exportKey('PEM'), - host_key=RSA.generate(2048).exportKey('PEM')) + user_key=user_secret_id, + host_key=host_secret_id) session.add(auth) try: session.commit() @@ -52,7 +63,7 @@ def createUserCert(session, user_id, auth_id, pub): certRecord = session.query(UserCert).get([user_id, fingerprint]) if certRecord is not None: return certRecord - cert = generateCert(auth.user_key, pub, principals='admin,root') + cert = generateCert(get_secret(auth.user_key), pub, principals='admin,root') if cert is None: raise falcon.HTTPInternalServerError("Failed to generate the certificate") user = UserCert( @@ -137,7 +148,7 @@ def createHostCert(session, token_id, host_id, pub): certRecord = session.query(HostCert).get([host_id, fingerprint]) if certRecord is not None: raise falcon.HTTPConflict('This public key is already signed.') - cert = generateCert(auth.host_key, pub, hostname=token.hostname) + cert = generateCert(get_secret(auth.host_key), pub, hostname=token.hostname) if cert == '': raise falcon.HTTPInternalServerError("Failed to generate the certificate") host = HostCert(host_id=host_id,