diff --git a/README.rst b/README.rst index 3d79018..ff897ce 100644 --- a/README.rst +++ b/README.rst @@ -159,6 +159,13 @@ The authorization is enabled using the python package Authorization could be enhanced by changing the method "decorated" using @auth.get_password in medallion/__init__.py +Scripts are also provided to run medallion through gunicorn with certificate- +based authentication: + +``` +MEDALLION_CONFFILE="" gunicorn -w 4 -b 0.0.0.0:5000 --log-level 'debug' --worker-class 'medallion.scripts.cert_auth_worker.CertAuthWorker' --ca-certs '' --certfile '' --keyfile '' --cert-reqs 2 --do-handshake-on-connect "medallion.scripts.cert_auth_app:getApp()" +``` + Configs may also contain a "taxii" section as well, as shown below: .. code-block:: json diff --git a/medallion/scripts/cert_auth_app.py b/medallion/scripts/cert_auth_app.py new file mode 100644 index 0000000..1d0d870 --- /dev/null +++ b/medallion/scripts/cert_auth_app.py @@ -0,0 +1,75 @@ +from functools import wraps +import logging +import os + +import flask +from werkzeug.security import check_password_hash + +import medallion +from medallion import connect_to_backend, register_blueprints, set_config +from medallion.common import ( + APPLICATION_INSTANCE, get_application_instance_config_values +) +import medallion.config +from medallion.scripts.cert_auth_gunicorn import ClientAuthApplication + +log = logging.getLogger("medallion") + + +class CertAuth(object): + + def login_required(self, f): + @wraps(f) + def decorated_function(*args, **kwargs): + username = flask.request.headers.get('X-USER') + stored_password = medallion.get_pwd(username) + client_password = flask.request.headers.get('X-PASS') + if not check_password_hash(stored_password, client_password): + raise medallion.exceptions.BackendError("Unauthorized", 401) + + return f(*args, **kwargs) + return decorated_function + + +medallion.auth = CertAuth() + + +def getApp(): + # Gunicorn messes up argparse, so only use env vars + # use one or the other, not both + configuration = medallion.config.load_config( + os.environ.get( + "MEDALLION_CONFFILE", medallion.config.DEFAULT_CONFFILE + ), + os.environ.get( + "MEDALLION_CONFDIR", medallion.config.DEFAULT_CONFDIR + ), + ) + + ca_path = configuration.get('ca_path') + cert_path = configuration.get('cert_path') + key_path = configuration.get('key_path') + host = configuration.get('host') + port = configuration.get('port') + + set_config(APPLICATION_INSTANCE, "users", configuration) + set_config(APPLICATION_INSTANCE, "taxii", configuration) + set_config(APPLICATION_INSTANCE, "backend", configuration) + + APPLICATION_INSTANCE.medallion_backend = connect_to_backend(get_application_instance_config_values(APPLICATION_INSTANCE, "backend")) + if (not APPLICATION_INSTANCE.blueprints): + register_blueprints(APPLICATION_INSTANCE) + + app = ClientAuthApplication( + APPLICATION_INSTANCE, + ca_path, + cert_path, + key_path, + host, + port, + ) + return app.application + + +if __name__ == "__main__": + getApp().run() diff --git a/medallion/scripts/cert_auth_gunicorn.py b/medallion/scripts/cert_auth_gunicorn.py new file mode 100644 index 0000000..c5fc3ec --- /dev/null +++ b/medallion/scripts/cert_auth_gunicorn.py @@ -0,0 +1,39 @@ +# From https://eugene.kovalev.systems/posts/flask-client-side-tls-authentication/ + +from multiprocessing import cpu_count +from pathlib import Path + +import gunicorn.app.base + +NUMBER_OF_WORKERS = (cpu_count() * 2) + 1 + + +class ClientAuthApplication(gunicorn.app.base.BaseApplication): + def __init__(self, app, ca_path: Path, cert_path: Path, key_path: Path, + hostname='localhost', port=80, num_workers=NUMBER_OF_WORKERS, timeout=30): + self.options = { + 'loglevel': 'debug', + 'bind': '{}:{}'.format(hostname, port), + 'workers': num_workers, + 'worker_class': 'medallion.scripts.cert_auth_worker.CertAuthWorker', + 'timeout': timeout, + 'ca_certs': str(ca_path), + 'certfile': str(cert_path), + 'keyfile': str(key_path), + 'cert_reqs': 2, + 'do_handshake_on_connect': True + } + self.application = app + super().__init__() + + def init(self, parser, opts, args): + return super().init(parser, opts, args) + + def load_config(self): + config = dict([(key, value) for key, value in self.options.items() + if key in self.cfg.settings and value is not None]) + for key, value in config.items(): + self.cfg.set(key.lower(), value) + + def load(self): + return self.application diff --git a/medallion/scripts/cert_auth_worker.py b/medallion/scripts/cert_auth_worker.py new file mode 100644 index 0000000..ae617e0 --- /dev/null +++ b/medallion/scripts/cert_auth_worker.py @@ -0,0 +1,31 @@ +# From https://eugene.kovalev.systems/posts/flask-client-side-tls-authentication/ + +"""A worker for Gunicorn that sets the client's Common Name as the X-USER header + variable for every request after the client has been authenticated so that + a Flask application can contain the authorization logic. + Based on: https://gist.github.com/kgriffs/289206f07e23b9a30d29a2b23e28c41c""" + +import ssl + +from gunicorn.workers.sync import SyncWorker + + +class CertAuthWorker(SyncWorker): + """A custom worker for putting authentication information into the X-USER + header variable of each request.""" + def handle_request(self, listener, req, client, addr): + """Handles each incoming request after a client has been authenticated.""" + subject = dict([i for subtuple in client.getpeercert().get('subject') for i in subtuple]) + issuer = dict([i for subtuple in client.getpeercert().get('issuer') for i in subtuple]) + headers = dict(req.headers) + headers['X-USER'] = subject.get('commonName') + serial = client.getpeercert().get('serialNumber') + headers['X-PASS'] = serial + not_before = client.getpeercert().get('notBefore') + not_after = client.getpeercert().get('notAfter') + headers['X-NOT_BEFORE'] = ssl.cert_time_to_seconds(not_before) + headers['X-NOT_AFTER'] = ssl.cert_time_to_seconds(not_after) + headers['X-ISSUER'] = issuer['commonName'] + + req.headers = list(headers.items()) + super(CertAuthWorker, self).handle_request(listener, req, client, addr) diff --git a/setup.py b/setup.py index 2bceede..1f128bd 100644 --- a/setup.py +++ b/setup.py @@ -77,6 +77,9 @@ def get_long_description(): "mongo": [ "pymongo", ], + "certauth": [ + "gunicorn", + ], }, project_urls={ 'Documentation': 'https://medallion.readthedocs.io/',