mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	tests: Preliminary tests for Kerberos/LDAP auth
This commit is contained in:
		| @@ -14,7 +14,7 @@ install: | |||||||
|   - echo "127.0.0.1 www.example.lan www" | sudo tee -a /etc/hosts |   - echo "127.0.0.1 www.example.lan www" | sudo tee -a /etc/hosts | ||||||
|   - sudo mkdir -p /etc/systemd/system |   - sudo mkdir -p /etc/systemd/system | ||||||
|   - sudo pip install -r requirements.txt |   - sudo pip install -r requirements.txt | ||||||
|   - sudo pip install codecov pytest-cov |   - sudo pip install codecov pytest-cov requests-kerberos | ||||||
|   - sudo pip install -e . |   - sudo pip install -e . | ||||||
| script: | script: | ||||||
|   - sudo find /home/ -type d -exec chmod 755 {} \; # Allow certidude serve to read templates |   - sudo find /home/ -type d -exec chmod 755 {} \; # Allow certidude serve to read templates | ||||||
|   | |||||||
| @@ -212,6 +212,12 @@ def certidude_app(log_handlers=[]): | |||||||
|     # Add sink for serving static files |     # Add sink for serving static files | ||||||
|     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) |     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) | ||||||
|  |  | ||||||
|  |     def log_exceptions(ex, req, resp, params): | ||||||
|  |         logger.debug("Caught exception: %s" % ex) | ||||||
|  |         raise ex | ||||||
|  |  | ||||||
|  |     app.add_error_handler(Exception, log_exceptions) | ||||||
|  |  | ||||||
|     # Set up log handlers |     # Set up log handlers | ||||||
|     if config.LOGGING_BACKEND == "sql": |     if config.LOGGING_BACKEND == "sql": | ||||||
|         from certidude.mysqllog import LogHandler |         from certidude.mysqllog import LogHandler | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ class RequestListResource(object): | |||||||
|         """ |         """ | ||||||
|         Validate and parse certificate signing request |         Validate and parse certificate signing request | ||||||
|         """ |         """ | ||||||
|         reason = "No reason" |         reasons = [] | ||||||
|         body = req.stream.read(req.content_length) |         body = req.stream.read(req.content_length) | ||||||
|         csr = x509.load_pem_x509_csr(body, default_backend()) |         csr = x509.load_pem_x509_csr(body, default_backend()) | ||||||
|         try: |         try: | ||||||
| @@ -79,7 +79,7 @@ class RequestListResource(object): | |||||||
|                     renewal_signature = b64decode(renewal_header) |                     renewal_signature = b64decode(renewal_header) | ||||||
|                 except TypeError, ValueError: |                 except TypeError, ValueError: | ||||||
|                     logger.error("Renewal failed, bad signature supplied for %s", common_name.value) |                     logger.error("Renewal failed, bad signature supplied for %s", common_name.value) | ||||||
|                     reason = "Renewal failed, bad signature supplied" |                     reasons.append("Renewal failed, bad signature supplied") | ||||||
|                 else: |                 else: | ||||||
|                     try: |                     try: | ||||||
|                         verifier = cert.public_key().verifier( |                         verifier = cert.public_key().verifier( | ||||||
| @@ -95,15 +95,15 @@ class RequestListResource(object): | |||||||
|                         verifier.verify() |                         verifier.verify() | ||||||
|                     except InvalidSignature: |                     except InvalidSignature: | ||||||
|                         logger.error("Renewal failed, invalid signature supplied for %s", common_name.value) |                         logger.error("Renewal failed, invalid signature supplied for %s", common_name.value) | ||||||
|                         reason = "Renewal failed, invalid signature supplied" |                         reasons.append("Renewal failed, invalid signature supplied") | ||||||
|                     else: |                     else: | ||||||
|                         # At this point renewal signature was valid but we need to perform some extra checks |                         # At this point renewal signature was valid but we need to perform some extra checks | ||||||
|                         if datetime.utcnow() > cert.not_valid_after: |                         if datetime.utcnow() > cert.not_valid_after: | ||||||
|                             logger.error("Renewal failed, current certificate for %s has expired", common_name.value) |                             logger.error("Renewal failed, current certificate for %s has expired", common_name.value) | ||||||
|                             reason = "Renewal failed, current certificate expired" |                             reasons.append("Renewal failed, current certificate expired") | ||||||
|                         elif not config.CERTIFICATE_RENEWAL_ALLOWED: |                         elif not config.CERTIFICATE_RENEWAL_ALLOWED: | ||||||
|                             logger.error("Renewal requested for %s, but not allowed by authority settings", common_name.value) |                             logger.error("Renewal requested for %s, but not allowed by authority settings", common_name.value) | ||||||
|                             reason = "Renewal requested, but not allowed by authority settings" |                             reasons.append("Renewal requested, but not allowed by authority settings") | ||||||
|                         else: |                         else: | ||||||
|                             resp.set_header("Content-Type", "application/x-x509-user-cert") |                             resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||||
|                             _, resp.body = authority._sign(csr, body, overwrite=True) |                             _, resp.body = authority._sign(csr, body, overwrite=True) | ||||||
| @@ -117,7 +117,6 @@ class RequestListResource(object): | |||||||
|         """ |         """ | ||||||
|         if req.get_param_as_bool("autosign"): |         if req.get_param_as_bool("autosign"): | ||||||
|             if "." not in common_name.value: |             if "." not in common_name.value: | ||||||
|                 reason = "Autosign failed, IP address not whitelisted" |  | ||||||
|                 for subnet in config.AUTOSIGN_SUBNETS: |                 for subnet in config.AUTOSIGN_SUBNETS: | ||||||
|                     if req.context.get("remote_addr") in subnet: |                     if req.context.get("remote_addr") in subnet: | ||||||
|                         try: |                         try: | ||||||
| @@ -128,16 +127,18 @@ class RequestListResource(object): | |||||||
|                         except EnvironmentError: |                         except EnvironmentError: | ||||||
|                             logger.info("Autosign for %s from %s failed, signed certificate already exists", |                             logger.info("Autosign for %s from %s failed, signed certificate already exists", | ||||||
|                                 common_name.value, req.context.get("remote_addr")) |                                 common_name.value, req.context.get("remote_addr")) | ||||||
|                             reason = "Autosign failed, signed certificate already exists" |                             reasons.append("Autosign failed, signed certificate already exists") | ||||||
|                         break |                         break | ||||||
|  |                 else: | ||||||
|  |                     reasons.append("Autosign failed, IP address not whitelisted") | ||||||
|             else: |             else: | ||||||
|                 reason = "Autosign failed, only client certificates allowed to be signed automatically" |                 reasons.append("Autosign failed, only client certificates allowed to be signed automatically") | ||||||
|  |  | ||||||
|         # Attempt to save the request otherwise |         # Attempt to save the request otherwise | ||||||
|         try: |         try: | ||||||
|             csr = authority.store_request(body) |             csr = authority.store_request(body) | ||||||
|         except errors.RequestExists: |         except errors.RequestExists: | ||||||
|             reason = "Same request already uploaded exists" |             reasons.append("Same request already uploaded exists") | ||||||
|             # We should still redirect client to long poll URL below |             # We should still redirect client to long poll URL below | ||||||
|         except errors.DuplicateCommonNameError: |         except errors.DuplicateCommonNameError: | ||||||
|             # TODO: Certificate renewal |             # TODO: Certificate renewal | ||||||
| @@ -161,7 +162,7 @@ class RequestListResource(object): | |||||||
|         else: |         else: | ||||||
|             # Request was accepted, but not processed |             # Request was accepted, but not processed | ||||||
|             resp.status = falcon.HTTP_202 |             resp.status = falcon.HTTP_202 | ||||||
|             resp.body = reason |             resp.body = ". ".join(reasons) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RequestDetailResource(object): | class RequestDetailResource(object): | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
|  |  | ||||||
| import click | import click | ||||||
|  | import gssapi | ||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| @@ -12,25 +13,12 @@ from certidude import config, const | |||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger("api") | ||||||
|  |  | ||||||
| if "kerberos" in config.AUTHENTICATION_BACKENDS: | os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB | ||||||
|     import gssapi |  | ||||||
|     os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB |  | ||||||
|     server_creds = gssapi.creds.Credentials( |  | ||||||
|         usage='accept', |  | ||||||
|         name=gssapi.names.Name('HTTP/%s'% (socket.gethostname()))) |  | ||||||
|     click.echo("Accepting requests only for realm: %s" % const.DOMAIN) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def authenticate(optional=False): | def authenticate(optional=False): | ||||||
|     import falcon |     import falcon | ||||||
|     def wrapper(func): |     def wrapper(func): | ||||||
|         def kerberos_authenticate(resource, req, resp, *args, **kwargs): |         def kerberos_authenticate(resource, req, resp, *args, **kwargs): | ||||||
|             # If LDAP enabled and device is not Kerberos capable fall |  | ||||||
|             # back to LDAP bind authentication |  | ||||||
|             if "ldap" in config.AUTHENTICATION_BACKENDS: |  | ||||||
|                 if "Android" in req.user_agent or "iPhone" in req.user_agent: |  | ||||||
|                     return ldap_authenticate(resource, req, resp, *args, **kwargs) |  | ||||||
|  |  | ||||||
|             # Try pre-emptive authentication |             # Try pre-emptive authentication | ||||||
|             if not req.auth: |             if not req.auth: | ||||||
|                 if optional: |                 if optional: | ||||||
| @@ -43,9 +31,22 @@ def authenticate(optional=False): | |||||||
|                     "No Kerberos ticket offered, are you sure you've logged in with domain user account?", |                     "No Kerberos ticket offered, are you sure you've logged in with domain user account?", | ||||||
|                     ["Negotiate"]) |                     ["Negotiate"]) | ||||||
|  |  | ||||||
|  |             server_creds = gssapi.creds.Credentials( | ||||||
|  |                 usage='accept', | ||||||
|  |                 name=gssapi.names.Name('HTTP/%s'% const.FQDN)) | ||||||
|  |  | ||||||
|             context = gssapi.sec_contexts.SecurityContext(creds=server_creds) |             context = gssapi.sec_contexts.SecurityContext(creds=server_creds) | ||||||
|  |  | ||||||
|  |             if not req.auth.startswith("Negotiate "): | ||||||
|  |                 raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth) | ||||||
|  |  | ||||||
|             token = ''.join(req.auth.split()[1:]) |             token = ''.join(req.auth.split()[1:]) | ||||||
|             context.step(b64decode(token)) |  | ||||||
|  |             try: | ||||||
|  |                 context.step(b64decode(token)) | ||||||
|  |             except TypeError: # base64 errors | ||||||
|  |                 raise falcon.HTTPBadRequest("Bad request", "Malformed token") | ||||||
|  |  | ||||||
|             username, domain = str(context.initiator_name).split("@") |             username, domain = str(context.initiator_name).split("@") | ||||||
|  |  | ||||||
|             if domain.lower() != const.DOMAIN.lower(): |             if domain.lower() != const.DOMAIN.lower(): | ||||||
| @@ -82,7 +83,7 @@ def authenticate(optional=False): | |||||||
|                     ("Basic",)) |                     ("Basic",)) | ||||||
|  |  | ||||||
|             if not req.auth.startswith("Basic "): |             if not req.auth.startswith("Basic "): | ||||||
|                 raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth) |                 raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth) | ||||||
|  |  | ||||||
|             from base64 import b64decode |             from base64 import b64decode | ||||||
|             basic, token = req.auth.split(" ", 1) |             basic, token = req.auth.split(" ", 1) | ||||||
| @@ -127,7 +128,7 @@ def authenticate(optional=False): | |||||||
|                 raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",)) |                 raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",)) | ||||||
|  |  | ||||||
|             if not req.auth.startswith("Basic "): |             if not req.auth.startswith("Basic "): | ||||||
|                 raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth) |                 raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth) | ||||||
|  |  | ||||||
|             basic, token = req.auth.split(" ", 1) |             basic, token = req.auth.split(" ", 1) | ||||||
|             user, passwd = b64decode(token).split(":", 1) |             user, passwd = b64decode(token).split(":", 1) | ||||||
| @@ -142,14 +143,21 @@ def authenticate(optional=False): | |||||||
|             req.context["user"] = User.objects.get(user) |             req.context["user"] = User.objects.get(user) | ||||||
|             return func(resource, req, resp, *args, **kwargs) |             return func(resource, req, resp, *args, **kwargs) | ||||||
|  |  | ||||||
|         if "kerberos" in config.AUTHENTICATION_BACKENDS: |         def wrapped(*args, **kwargs): | ||||||
|             return kerberos_authenticate |             # If LDAP enabled and device is not Kerberos capable fall | ||||||
|         elif config.AUTHENTICATION_BACKENDS == {"pam"}: |             # back to LDAP bind authentication | ||||||
|             return pam_authenticate |             if "ldap" in config.AUTHENTICATION_BACKENDS: | ||||||
|         elif config.AUTHENTICATION_BACKENDS == {"ldap"}: |                 if "Android" in req.user_agent or "iPhone" in req.user_agent: | ||||||
|             return ldap_authenticate |                     return ldap_authenticate(resource, req, resp, *args, **kwargs) | ||||||
|         else: |             if "kerberos" in config.AUTHENTICATION_BACKENDS: | ||||||
|             raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS) |                 return kerberos_authenticate(*args, **kwargs) | ||||||
|  |             elif config.AUTHENTICATION_BACKENDS == {"pam"}: | ||||||
|  |                 return pam_authenticate(*args, **kwargs) | ||||||
|  |             elif config.AUTHENTICATION_BACKENDS == {"ldap"}: | ||||||
|  |                 return ldap_authenticate(*args, **kwargs) | ||||||
|  |             else: | ||||||
|  |                 raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS) | ||||||
|  |         return wrapped | ||||||
|     return wrapper |     return wrapper | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -134,11 +134,10 @@ def revoke(common_name): | |||||||
|     push.publish("certificate-revoked", common_name) |     push.publish("certificate-revoked", common_name) | ||||||
|  |  | ||||||
|     # Publish CRL for long polls |     # Publish CRL for long polls | ||||||
|     if config.LONG_POLL_PUBLISH: |     url = config.LONG_POLL_PUBLISH % "crl" | ||||||
|         url = config.LONG_POLL_PUBLISH % "crl" |     click.echo("Publishing CRL at %s ..." % url) | ||||||
|         click.echo("Publishing CRL at %s ..." % url) |     requests.post(url, data=export_crl(), | ||||||
|         requests.post(url, data=export_crl(), |         headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"}) | ||||||
|             headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"}) |  | ||||||
|  |  | ||||||
|     attach_cert = buf, "application/x-pem-file", common_name + ".crt" |     attach_cert = buf, "application/x-pem-file", common_name + ".crt" | ||||||
|     mailer.send("certificate-revoked.md", |     mailer.send("certificate-revoked.md", | ||||||
| @@ -220,10 +219,9 @@ def delete_request(common_name): | |||||||
|     push.publish("request-deleted", common_name) |     push.publish("request-deleted", common_name) | ||||||
|  |  | ||||||
|     # Write empty certificate to long-polling URL |     # Write empty certificate to long-polling URL | ||||||
|     if config.LONG_POLL_PUBLISH: |     requests.delete( | ||||||
|         requests.delete( |         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), | ||||||
|             config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), |         headers={"User-Agent": "Certidude API"}) | ||||||
|             headers={"User-Agent": "Certidude API"}) |  | ||||||
|  |  | ||||||
| def generate_ovpn_bundle(common_name, owner=None): | def generate_ovpn_bundle(common_name, owner=None): | ||||||
|     # Construct private key |     # Construct private key | ||||||
| @@ -370,13 +368,10 @@ def _sign(csr, buf, overwrite=False): | |||||||
|     else: # New keypair |     else: # New keypair | ||||||
|         mailer.send("certificate-signed.md", **locals()) |         mailer.send("certificate-signed.md", **locals()) | ||||||
|  |  | ||||||
|     if config.LONG_POLL_PUBLISH: |     url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() | ||||||
|         url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() |     click.echo("Publishing certificate at %s ..." % url) | ||||||
|         click.echo("Publishing certificate at %s ..." % url) |     requests.post(url, data=cert_buf, | ||||||
|         requests.post(url, data=cert_buf, |         headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) | ||||||
|             headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) |  | ||||||
|  |  | ||||||
|     if config.EVENT_SOURCE_PUBLISH: # TODO: handle renewal |  | ||||||
|         push.publish("request-signed", common_name.value) |  | ||||||
|  |  | ||||||
|  |     push.publish("request-signed", common_name.value) | ||||||
|     return cert, cert_buf |     return cert, cert_buf | ||||||
|   | |||||||
| @@ -728,8 +728,9 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat | |||||||
| @fqdn_required | @fqdn_required | ||||||
| def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags): | def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags): | ||||||
|     # Install only rarely changing stuff from OS package management |     # Install only rarely changing stuff from OS package management | ||||||
|     apt("python-setproctitle cython python-dev libkrb5-dev libldap2-dev libffi-dev libssl-dev") |     apt("python-setproctitle cython python-dev libkrb5-dev libffi-dev libssl-dev") | ||||||
|     apt("python-mimeparse python-markdown python-xattr python-jinja2 python-cffi python-openssl software-properties-common") |     apt("python-mimeparse python-markdown python-xattr python-jinja2 python-cffi") | ||||||
|  |     apt("python-ldap python-openssl software-properties-common libsasl2-modules-gssapi-mit") | ||||||
|     pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests") |     pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests") | ||||||
|     click.echo("Software dependencies installed") |     click.echo("Software dependencies installed") | ||||||
|  |  | ||||||
| @@ -1086,10 +1087,11 @@ def certidude_cron(): | |||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("serve", help="Run server") | @click.command("serve", help="Run server") | ||||||
|  | @click.option("-e", "--exit-handler", default=False, is_flag=True, help="Install /api/exit/ handler") | ||||||
| @click.option("-p", "--port", default=80, help="Listen port") | @click.option("-p", "--port", default=80, help="Listen port") | ||||||
| @click.option("-l", "--listen", default="0.0.0.0", help="Listen address") | @click.option("-l", "--listen", default="0.0.0.0", help="Listen address") | ||||||
| @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") | @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") | ||||||
| def certidude_serve(port, listen, fork): | def certidude_serve(port, listen, fork, exit_handler): | ||||||
|     from setproctitle import setproctitle |     from setproctitle import setproctitle | ||||||
|     from certidude.signer import SignServer |     from certidude.signer import SignServer | ||||||
|     from certidude import authority, const |     from certidude import authority, const | ||||||
| @@ -1177,16 +1179,13 @@ def certidude_serve(port, listen, fork): | |||||||
|  |  | ||||||
|     click.echo("Serving API at %s:%d" % (listen, port)) |     click.echo("Serving API at %s:%d" % (listen, port)) | ||||||
|     from wsgiref.simple_server import make_server, WSGIServer |     from wsgiref.simple_server import make_server, WSGIServer | ||||||
|     from SocketServer import ForkingMixIn |  | ||||||
|     from certidude.api import certidude_app |     from certidude.api import certidude_app | ||||||
|  |  | ||||||
|     class ThreadingWSGIServer(ForkingMixIn, WSGIServer): |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
|     click.echo("Listening on %s:%d" % (listen, port)) |     click.echo("Listening on %s:%d" % (listen, port)) | ||||||
|  |  | ||||||
|     app = certidude_app(log_handlers) |     app = certidude_app(log_handlers) | ||||||
|     httpd = make_server(listen, port, app, ThreadingWSGIServer) |     httpd = make_server(listen, port, app, WSGIServer) | ||||||
|  |  | ||||||
|  |  | ||||||
|     """ |     """ | ||||||
| @@ -1198,9 +1197,8 @@ def certidude_serve(port, listen, fork): | |||||||
|     if os.path.exists("/etc/cron.hourly/certidude"): |     if os.path.exists("/etc/cron.hourly/certidude"): | ||||||
|         os.system("/etc/cron.hourly/certidude") |         os.system("/etc/cron.hourly/certidude") | ||||||
|  |  | ||||||
|     if config.EVENT_SOURCE_PUBLISH: |     from certidude.push import EventSourceLogHandler | ||||||
|         from certidude.push import EventSourceLogHandler |     log_handlers.append(EventSourceLogHandler()) | ||||||
|         log_handlers.append(EventSourceLogHandler()) |  | ||||||
|  |  | ||||||
|     for j in logging.Logger.manager.loggerDict.values(): |     for j in logging.Logger.manager.loggerDict.values(): | ||||||
|         if isinstance(j, logging.Logger): # PlaceHolder is what? |         if isinstance(j, logging.Logger): # PlaceHolder is what? | ||||||
| @@ -1222,14 +1220,18 @@ def certidude_serve(port, listen, fork): | |||||||
|         logger.debug("Started Certidude at %s", const.FQDN) |         logger.debug("Started Certidude at %s", const.FQDN) | ||||||
|  |  | ||||||
|         drop_privileges() |         drop_privileges() | ||||||
|         def quit_handler(*args, **kwargs): |  | ||||||
|             click.echo("Shutting down HTTP server...") |         class ExitResource(): | ||||||
|             raise KeyboardInterrupt |             """ | ||||||
|         signal.signal(signal.SIGHUP, quit_handler) |             Provide way to gracefully shutdown server | ||||||
|         try: |             """ | ||||||
|             httpd.serve_forever() |             def on_get(self, req, resp): | ||||||
|         except KeyboardInterrupt: |                 assert httpd._BaseServer__shutdown_request == False | ||||||
|             click.echo("Caught Ctrl-C, server stopped") |                 httpd._BaseServer__shutdown_request = True | ||||||
|  |  | ||||||
|  |         if exit_handler: | ||||||
|  |             app.add_route("/api/exit/", ExitResource()) | ||||||
|  |         httpd.serve_forever() | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("yubikey", help="Set up Yubikey as client authentication token") | @click.command("yubikey", help="Set up Yubikey as client authentication token") | ||||||
|   | |||||||
| @@ -73,19 +73,14 @@ LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") | |||||||
|  |  | ||||||
| LOGGING_BACKEND = cp.get("logging", "backend") | LOGGING_BACKEND = cp.get("logging", "backend") | ||||||
|  |  | ||||||
| if "whitelist" == AUTHORIZATION_BACKEND: | USERS_WHITELIST = set([j for j in  cp.get("authorization", "user whitelist").split(" ") if j]) | ||||||
|     USERS_WHITELIST = set([j for j in  cp.get("authorization", "users whitelist").split(" ") if j]) | ADMINS_WHITELIST = set([j for j in  cp.get("authorization", "admin whitelist").split(" ") if j]) | ||||||
|     ADMINS_WHITELIST = set([j for j in  cp.get("authorization", "admins whitelist").split(" ") if j]) | USERS_GROUP = cp.get("authorization", "posix user group") | ||||||
| elif "posix" == AUTHORIZATION_BACKEND: | ADMIN_GROUP = cp.get("authorization", "posix admin group") | ||||||
|     USERS_GROUP = cp.get("authorization", "posix user group") | LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") | ||||||
|     ADMIN_GROUP = cp.get("authorization", "posix admin group") | LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") | ||||||
| elif "ldap" == AUTHORIZATION_BACKEND: | if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") | ||||||
|     LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") | if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") | ||||||
|     LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") |  | ||||||
|     if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") |  | ||||||
|     if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") |  | ||||||
| else: |  | ||||||
|     raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) |  | ||||||
|  |  | ||||||
| TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("tagging")] | TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("tagging")] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,9 +13,6 @@ def publish(event_type, event_data): | |||||||
|     """ |     """ | ||||||
|     assert event_type, "No event type specified" |     assert event_type, "No event type specified" | ||||||
|     assert event_data, "No event data specified" |     assert event_data, "No event data specified" | ||||||
|     if not config.EVENT_SOURCE_PUBLISH: |  | ||||||
|         # Push server disabled |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|     if not isinstance(event_data, basestring): |     if not isinstance(event_data, basestring): | ||||||
|         from certidude.decorators import MyEncoder |         from certidude.decorators import MyEncoder | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ backends = pam | |||||||
| ;backends = ldap | ;backends = ldap | ||||||
| ;backends = kerberos ldap | ;backends = kerberos ldap | ||||||
| ;backends = kerberos pam | ;backends = kerberos pam | ||||||
| ldap uri = ldaps://dc1.example.com | ldap uri = ldaps://dc.example.lan | ||||||
| kerberos keytab = FILE:{{ kerberos_keytab }} | kerberos keytab = FILE:{{ kerberos_keytab }} | ||||||
|  |  | ||||||
| [accounts] | [accounts] | ||||||
| @@ -23,8 +23,8 @@ kerberos keytab = FILE:{{ kerberos_keytab }} | |||||||
| backend = posix | backend = posix | ||||||
| ;backend = ldap | ;backend = ldap | ||||||
| ldap gssapi credential cache = /run/certidude/krb5cc | ldap gssapi credential cache = /run/certidude/krb5cc | ||||||
| ldap uri = ldap://dc1.example.com | ldap uri = ldap://dc.example.lan | ||||||
| ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=com{% endif %} | ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=lan{% endif %} | ||||||
|  |  | ||||||
| [authorization] | [authorization] | ||||||
| # The authorization backend specifies how the users are authorized. | # The authorization backend specifies how the users are authorized. | ||||||
| @@ -38,7 +38,11 @@ posix admin group = sudo | |||||||
| ;backend = ldap | ;backend = ldap | ||||||
| ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s)) | ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s)) | ||||||
| ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s)) | ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s)) | ||||||
| ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{% if base %}{{ base }}{% else %}dc=example,dc=com{% endif %})(samaccountname=%s)) | ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{% if base %}{{ base }}{% else %}dc=example,dc=lan{% endif %})(samaccountname=%s)) | ||||||
|  |  | ||||||
|  | ;backend = whitelist | ||||||
|  | user whitelist = | ||||||
|  | admin whitelist = | ||||||
|  |  | ||||||
| # Users are allowed to log in from user subnets | # Users are allowed to log in from user subnets | ||||||
| user subnets = 0.0.0.0/0 | user subnets = 0.0.0.0/0 | ||||||
|   | |||||||
| @@ -93,6 +93,7 @@ def clean_server(): | |||||||
|             except OSError: |             except OSError: | ||||||
|                 pass |                 pass | ||||||
|  |  | ||||||
|  |  | ||||||
|     if os.path.exists("/var/lib/certidude/ca.example.lan"): |     if os.path.exists("/var/lib/certidude/ca.example.lan"): | ||||||
|         shutil.rmtree("/var/lib/certidude/ca.example.lan") |         shutil.rmtree("/var/lib/certidude/ca.example.lan") | ||||||
|     if os.path.exists("/etc/certidude/server.conf"): |     if os.path.exists("/etc/certidude/server.conf"): | ||||||
| @@ -126,10 +127,25 @@ def clean_server(): | |||||||
|         if os.path.exists("/etc/openvpn/keys"): |         if os.path.exists("/etc/openvpn/keys"): | ||||||
|             shutil.rmtree("/etc/openvpn/keys") |             shutil.rmtree("/etc/openvpn/keys") | ||||||
|  |  | ||||||
|     # System packages |     # Stop samba | ||||||
|     os.system("apt purge -y nginx libnginx-mod-nchan openvpn strongswan") |     if os.path.exists("/run/samba/samba.pid"): | ||||||
|     os.system("apt-get -y autoremove") |         with open("/run/samba/samba.pid") as fh: | ||||||
|  |             try: | ||||||
|  |                 os.kill(int(fh.read()), 15) | ||||||
|  |             except OSError: | ||||||
|  |                 pass | ||||||
|  |     if os.path.exists("/etc/krb5.conf"): | ||||||
|  |         os.unlink("/etc/krb5.conf") | ||||||
|  |     if os.path.exists("/etc/krb5.keytab"): | ||||||
|  |         os.unlink("/etc/krb5.keytab") | ||||||
|  |     if os.path.exists("/etc/certidude/server.keytab"): | ||||||
|  |         os.unlink("/etc/certidude/server.keytab") | ||||||
|  |     if os.path.exists("/var/lib/samba/"): | ||||||
|  |         shutil.rmtree("/var/lib/samba") | ||||||
|  |     os.makedirs("/var/lib/samba") | ||||||
|  |  | ||||||
|  |     # Restore initial resolv.conf | ||||||
|  |     shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf") | ||||||
|  |  | ||||||
| def test_cli_setup_authority(): | def test_cli_setup_authority(): | ||||||
|     import os |     import os | ||||||
| @@ -137,9 +153,30 @@ def test_cli_setup_authority(): | |||||||
|  |  | ||||||
|     assert os.getuid() == 0, "Run tests as root in a clean VM or container" |     assert os.getuid() == 0, "Run tests as root in a clean VM or container" | ||||||
|  |  | ||||||
|  |     if not os.path.exists("/etc/resolv.conf.orig"): | ||||||
|  |         shutil.copyfile("/etc/resolv.conf", "/etc/resolv.conf.orig") | ||||||
|  |  | ||||||
|     clean_server() |     clean_server() | ||||||
|     clean_client() |     clean_client() | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # Bootstrap domain controller here, | ||||||
|  |     # Samba startup takes some time | ||||||
|  |     os.system("apt install -y samba krb5-user winbind") | ||||||
|  |     if os.path.exists("/etc/samba/smb.conf"): | ||||||
|  |         os.unlink("/etc/samba/smb.conf") | ||||||
|  |     os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca") | ||||||
|  |     os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'") | ||||||
|  |     os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'") | ||||||
|  |     os.system("samba-tool user setpassword administrator --newpassword=S4l4k4l4") | ||||||
|  |     os.symlink("/var/lib/samba/private/secrets.keytab", "/etc/krb5.keytab") | ||||||
|  |     os.chmod("/var/lib/samba/private/secrets.keytab", 0644) # To allow access to certidude server | ||||||
|  |     os.symlink("/var/lib/samba/private/krb5.conf", "/etc/krb5.conf") | ||||||
|  |     with open("/etc/resolv.conf", "w") as fh: | ||||||
|  |         fh.write("nameserver 127.0.0.1\nsearch example.lan\n") | ||||||
|  |     # TODO: dig -t srv perhaps? | ||||||
|  |     os.system("samba") | ||||||
|  |  | ||||||
|     from certidude.cli import entry_point as cli |     from certidude.cli import entry_point as cli | ||||||
|     from certidude import const |     from certidude import const | ||||||
|  |  | ||||||
| @@ -156,7 +193,7 @@ def test_cli_setup_authority(): | |||||||
|     assert os.system("nginx -t") == 0, "invalid nginx configuration" |     assert os.system("nginx -t") == 0, "invalid nginx configuration" | ||||||
|     assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly" |     assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly" | ||||||
|  |  | ||||||
|     from certidude import config, authority |     from certidude import config, authority, auth, user | ||||||
|     assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000 |     assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000 | ||||||
|     assert authority.ca_cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff |     assert authority.ca_cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff | ||||||
|     assert authority.ca_cert.not_valid_before < datetime.now() |     assert authority.ca_cert.not_valid_before < datetime.now() | ||||||
| @@ -182,7 +219,7 @@ def test_cli_setup_authority(): | |||||||
|     server_pid = os.fork() |     server_pid = os.fork() | ||||||
|     if not server_pid: |     if not server_pid: | ||||||
|         # Fork to prevent umask, setuid, setgid side effects |         # Fork to prevent umask, setuid, setgid side effects | ||||||
|         result = runner.invoke(cli, ['serve', '-p', '8080', '-l', '127.0.1.1']) |         result = runner.invoke(cli, ['serve', '-p', '8080', '-l', '127.0.1.1', '-e']) | ||||||
|         assert not result.exception, result.output |         assert not result.exception, result.output | ||||||
|         return |         return | ||||||
|  |  | ||||||
| @@ -519,15 +556,13 @@ def test_cli_setup_authority(): | |||||||
|     # Test session API call |     # Test session API call | ||||||
|     r = client().simulate_get("/api/", headers={"Authorization":usertoken}) |     r = client().simulate_get("/api/", headers={"Authorization":usertoken}) | ||||||
|     assert r.status_code == 200 |     assert r.status_code == 200 | ||||||
|  |  | ||||||
|     r = client().simulate_get("/api/", headers={"Authorization":admintoken}) |     r = client().simulate_get("/api/", headers={"Authorization":admintoken}) | ||||||
|     assert r.status_code == 200 |     assert r.status_code == 200 | ||||||
|  |  | ||||||
|     r = client().simulate_get("/api/", headers={"Accept":"text/plain", "Authorization":admintoken}) |     r = client().simulate_get("/api/", headers={"Accept":"text/plain", "Authorization":admintoken}) | ||||||
|     assert r.status_code == 415 # invalid media type |     assert r.status_code == 415 # invalid media type | ||||||
|  |  | ||||||
|     r = client().simulate_get("/api/") |     r = client().simulate_get("/api/") | ||||||
|     assert r.status_code == 401 |     assert r.status_code == 401 | ||||||
|  |     assert "Please authenticate" in r.text | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Test token mech |     # Test token mech | ||||||
| @@ -568,9 +603,14 @@ def test_cli_setup_authority(): | |||||||
|     # Beyond this point don't use client() |     # Beyond this point don't use client() | ||||||
|     const.STORAGE_PATH = "/tmp/" |     const.STORAGE_PATH = "/tmp/" | ||||||
|  |  | ||||||
|  |  | ||||||
|     ############# |     ############# | ||||||
|     ### nginx ### |     ### nginx ### | ||||||
|     ############# |     ############# | ||||||
|  |  | ||||||
|  |     # In this case nginx is set up as web server with TLS certificates | ||||||
|  |     # generated by certidude. | ||||||
|  |  | ||||||
|     clean_client() |     clean_client() | ||||||
|  |  | ||||||
|     result = runner.invoke(cli, ["setup", "nginx", "-cn", "www", "ca.example.lan"]) |     result = runner.invoke(cli, ["setup", "nginx", "-cn", "www", "ca.example.lan"]) | ||||||
| @@ -612,11 +652,15 @@ def test_cli_setup_authority(): | |||||||
|     # Test nginx setup |     # Test nginx setup | ||||||
|     assert os.system("nginx -t") == 0, "Generated nginx config was invalid" |     assert os.system("nginx -t") == 0, "Generated nginx config was invalid" | ||||||
|  |  | ||||||
|  |     # TODO: test client verification with curl | ||||||
|  |  | ||||||
|  |  | ||||||
|     ############### |     ############### | ||||||
|     ### OpenVPN ### |     ### OpenVPN ### | ||||||
|     ############### |     ############### | ||||||
|  |  | ||||||
|  |     # First OpenVPN server is set up | ||||||
|  |  | ||||||
|     clean_client() |     clean_client() | ||||||
|  |  | ||||||
|     if not os.path.exists("/etc/openvpn/keys"): |     if not os.path.exists("/etc/openvpn/keys"): | ||||||
| @@ -651,7 +695,8 @@ def test_cli_setup_authority(): | |||||||
|     assert os.path.exists("/tmp/ca.example.lan/server_cert.pem") |     assert os.path.exists("/tmp/ca.example.lan/server_cert.pem") | ||||||
|     assert os.path.exists("/etc/openvpn/site-to-client.conf") |     assert os.path.exists("/etc/openvpn/site-to-client.conf") | ||||||
|  |  | ||||||
|     # Reset config |     # Secondly OpenVPN client is set up | ||||||
|  |  | ||||||
|     os.unlink("/etc/certidude/client.conf") |     os.unlink("/etc/certidude/client.conf") | ||||||
|     os.unlink("/etc/certidude/services.conf") |     os.unlink("/etc/certidude/services.conf") | ||||||
|  |  | ||||||
| @@ -669,8 +714,9 @@ def test_cli_setup_authority(): | |||||||
|     assert "Writing certificate to:" in result.output, result.output |     assert "Writing certificate to:" in result.output, result.output | ||||||
|     assert os.path.exists("/etc/openvpn/client-to-site.conf") |     assert os.path.exists("/etc/openvpn/client-to-site.conf") | ||||||
|  |  | ||||||
|  |     # TODO: Check that tunnel interfaces came up, perhaps try to ping? | ||||||
|     # TODO: assert key, req, cert paths were included correctly in OpenVPN config |     # TODO: assert key, req, cert paths were included correctly in OpenVPN config | ||||||
|     # TODO: test client verification with curl |  | ||||||
|  |  | ||||||
|     ############### |     ############### | ||||||
|     ### IPSec ### |     ### IPSec ### | ||||||
| @@ -755,6 +801,74 @@ def test_cli_setup_authority(): | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     #################################### | ||||||
|  |     ### Switch to Kerberos/LDAP auth ### | ||||||
|  |     #################################### | ||||||
|  |  | ||||||
|  |     # Shut down current instance | ||||||
|  |     requests.get("http://ca.example.lan/api/exit") | ||||||
|  |     requests.get("http://ca.example.lan/api/") | ||||||
|  |     os.waitpid(server_pid, 0) | ||||||
|  |  | ||||||
|  |     # Hacks, note that CA is domain controller | ||||||
|  |     assert os.system("kdestroy") == 0 | ||||||
|  |     assert not os.path.exists("/tmp/krb5cc_0") | ||||||
|  |  | ||||||
|  |     assert os.system("echo S4l4k4l4 | kinit administrator") == 0 | ||||||
|  |     assert os.path.exists("/tmp/krb5cc_0") | ||||||
|  |     os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ") | ||||||
|  |  | ||||||
|  |     # Create service principals | ||||||
|  |     spn_pid = os.fork() | ||||||
|  |     if not spn_pid: | ||||||
|  |         assert os.getuid() == 0 and os.getgid() == 0 | ||||||
|  |         os.environ["KRB5_KTNAME"] = "FILE:/etc/certidude/server.keytab" | ||||||
|  |         assert os.system("net ads keytab add HTTP -k") == 0 | ||||||
|  |         assert os.path.exists("/etc/certidude/server.keytab") | ||||||
|  |         os.system("chown root:certidude /etc/certidude/server.keytab") | ||||||
|  |         os.system("chmod 640 /etc/certidude/server.keytab") | ||||||
|  |         return | ||||||
|  |     else: | ||||||
|  |         os.waitpid(spn_pid, 0) | ||||||
|  |  | ||||||
|  |     os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") | ||||||
|  |     os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") | ||||||
|  |     os.system("sed -e 's/backends = pam/backends = kerberos/g' -i /etc/certidude/server.conf") | ||||||
|  |     os.system("sed -e 's/backend = posix/backend = ldap/g' -i /etc/certidude/server.conf") | ||||||
|  |     os.system("/etc/cron.hourly/certidude") # Update server credential cache | ||||||
|  |  | ||||||
|  |     result = runner.invoke(cli, ['users']) | ||||||
|  |     assert not result.exception, result.output | ||||||
|  |     # TODO: assert "Administrator@example.lan" in result.output | ||||||
|  |  | ||||||
|  |     server_pid = os.fork() # Fork to prevent environment contamination | ||||||
|  |     if not server_pid: | ||||||
|  |         # Apply /etc/certidude/server.conf changes | ||||||
|  |         reload(config) | ||||||
|  |         reload(user) | ||||||
|  |         reload(auth) | ||||||
|  |  | ||||||
|  |         assert isinstance(user.User.objects, user.ActiveDirectoryUserManager), user.User.objects | ||||||
|  |         result = runner.invoke(cli, ['serve', '-p', '8080', '-l', '127.0.1.1', '-e']) | ||||||
|  |         assert not result.exception, result.output | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     sleep(1) # Wait for serve to start up | ||||||
|  |  | ||||||
|  |     # TODO: pip install requests-kerberos | ||||||
|  |     from requests_kerberos import HTTPKerberosAuth, OPTIONAL | ||||||
|  |     auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) | ||||||
|  |  | ||||||
|  |     # Test session API call | ||||||
|  |     r = requests.get("http://ca.example.lan/api/") | ||||||
|  |     assert r.status_code == 401, r.text | ||||||
|  |     assert "No Kerberos ticket offered" in r.text, r.text | ||||||
|  |     r = requests.get("http://ca.example.lan/api/", headers={"Authorization": "Negotiate blerrgh"}) | ||||||
|  |     assert r.status_code == 400, r.text | ||||||
|  |     r = requests.get("http://ca.example.lan/api/", auth=auth) | ||||||
|  |     assert r.status_code == 200, r.text | ||||||
|  |  | ||||||
|  |  | ||||||
|     ################### |     ################### | ||||||
|     ### Final tests ### |     ### Final tests ### | ||||||
|     ################### |     ################### | ||||||
| @@ -783,18 +897,18 @@ def test_cli_setup_authority(): | |||||||
|     assert authority.signer_exec("exit") == "ok" |     assert authority.signer_exec("exit") == "ok" | ||||||
|  |  | ||||||
|     # Shut down server |     # Shut down server | ||||||
|     with open("/run/certidude/server.pid") as fh: |     requests.get("http://ca.example.lan/api/exit") | ||||||
|         os.kill(int(fh.read()), 1) |     os.waitpid(server_pid, 0) | ||||||
|  |  | ||||||
|     # Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude |     # Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude | ||||||
|     assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == "/tmp/** r,\n" |     assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == "/tmp/** r,\n" | ||||||
|  |  | ||||||
|     assert len(inbox) == 0, inbox # Make sure all messages were checked |     assert len(inbox) == 0, inbox # Make sure all messages were checked | ||||||
|  |  | ||||||
|     os.waitpid(server_pid, 0) |  | ||||||
|  |  | ||||||
|     os.system("service nginx stop") |     os.system("service nginx stop") | ||||||
|     os.system("service openvpn stop") |     os.system("service openvpn stop") | ||||||
|     os.system("ipsec stop") |     os.system("ipsec stop") | ||||||
|  |  | ||||||
|     clean_server() |     clean_server() | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     test_cli_setup_authority() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user