diff --git a/.travis.yml b/.travis.yml index f2acf9a..7241690 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ install: - echo "127.0.0.1 www.example.lan www" | sudo tee -a /etc/hosts - sudo mkdir -p /etc/systemd/system - sudo pip install -r requirements.txt - - sudo pip install codecov pytest-cov + - sudo pip install codecov pytest-cov requests-kerberos - sudo pip install -e . script: - sudo find /home/ -type d -exec chmod 755 {} \; # Allow certidude serve to read templates diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 1d8b4d5..08466e6 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -212,6 +212,12 @@ def certidude_app(log_handlers=[]): # Add sink for serving static files 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 if config.LOGGING_BACKEND == "sql": from certidude.mysqllog import LogHandler diff --git a/certidude/api/request.py b/certidude/api/request.py index 50c0bbe..0bca54b 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -29,7 +29,7 @@ class RequestListResource(object): """ Validate and parse certificate signing request """ - reason = "No reason" + reasons = [] body = req.stream.read(req.content_length) csr = x509.load_pem_x509_csr(body, default_backend()) try: @@ -79,7 +79,7 @@ class RequestListResource(object): renewal_signature = b64decode(renewal_header) except TypeError, ValueError: 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: try: verifier = cert.public_key().verifier( @@ -95,15 +95,15 @@ class RequestListResource(object): verifier.verify() except InvalidSignature: 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: # At this point renewal signature was valid but we need to perform some extra checks if datetime.utcnow() > cert.not_valid_after: 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: 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: resp.set_header("Content-Type", "application/x-x509-user-cert") _, resp.body = authority._sign(csr, body, overwrite=True) @@ -117,7 +117,6 @@ class RequestListResource(object): """ if req.get_param_as_bool("autosign"): if "." not in common_name.value: - reason = "Autosign failed, IP address not whitelisted" for subnet in config.AUTOSIGN_SUBNETS: if req.context.get("remote_addr") in subnet: try: @@ -128,16 +127,18 @@ class RequestListResource(object): except EnvironmentError: logger.info("Autosign for %s from %s failed, signed certificate already exists", common_name.value, req.context.get("remote_addr")) - reason = "Autosign failed, signed certificate already exists" + reasons.append("Autosign failed, signed certificate already exists") break + else: + reasons.append("Autosign failed, IP address not whitelisted") 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 try: csr = authority.store_request(body) 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 except errors.DuplicateCommonNameError: # TODO: Certificate renewal @@ -161,7 +162,7 @@ class RequestListResource(object): else: # Request was accepted, but not processed resp.status = falcon.HTTP_202 - resp.body = reason + resp.body = ". ".join(reasons) class RequestDetailResource(object): diff --git a/certidude/auth.py b/certidude/auth.py index 40f88b5..684a954 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -1,5 +1,6 @@ import click +import gssapi import falcon import logging import os @@ -12,25 +13,12 @@ from certidude import config, const logger = logging.getLogger("api") -if "kerberos" in config.AUTHENTICATION_BACKENDS: - 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) - +os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB def authenticate(optional=False): import falcon def wrapper(func): 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 if not req.auth: 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?", ["Negotiate"]) + server_creds = gssapi.creds.Credentials( + usage='accept', + name=gssapi.names.Name('HTTP/%s'% const.FQDN)) + 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:]) - 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("@") if domain.lower() != const.DOMAIN.lower(): @@ -82,7 +83,7 @@ def authenticate(optional=False): ("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 basic, token = req.auth.split(" ", 1) @@ -127,7 +128,7 @@ def authenticate(optional=False): raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("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) user, passwd = b64decode(token).split(":", 1) @@ -142,14 +143,21 @@ def authenticate(optional=False): req.context["user"] = User.objects.get(user) return func(resource, req, resp, *args, **kwargs) - if "kerberos" in config.AUTHENTICATION_BACKENDS: - return kerberos_authenticate - elif config.AUTHENTICATION_BACKENDS == {"pam"}: - return pam_authenticate - elif config.AUTHENTICATION_BACKENDS == {"ldap"}: - return ldap_authenticate - else: - raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS) + def wrapped(*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) + if "kerberos" in 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 diff --git a/certidude/authority.py b/certidude/authority.py index 8119f82..cde4810 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -134,11 +134,10 @@ def revoke(common_name): push.publish("certificate-revoked", common_name) # Publish CRL for long polls - if config.LONG_POLL_PUBLISH: - url = config.LONG_POLL_PUBLISH % "crl" - click.echo("Publishing CRL at %s ..." % url) - requests.post(url, data=export_crl(), - headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"}) + url = config.LONG_POLL_PUBLISH % "crl" + click.echo("Publishing CRL at %s ..." % url) + requests.post(url, data=export_crl(), + headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"}) attach_cert = buf, "application/x-pem-file", common_name + ".crt" mailer.send("certificate-revoked.md", @@ -220,10 +219,9 @@ def delete_request(common_name): push.publish("request-deleted", common_name) # Write empty certificate to long-polling URL - if config.LONG_POLL_PUBLISH: - requests.delete( - config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), - headers={"User-Agent": "Certidude API"}) + requests.delete( + config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), + headers={"User-Agent": "Certidude API"}) def generate_ovpn_bundle(common_name, owner=None): # Construct private key @@ -370,13 +368,10 @@ def _sign(csr, buf, overwrite=False): else: # New keypair mailer.send("certificate-signed.md", **locals()) - if config.LONG_POLL_PUBLISH: - url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() - click.echo("Publishing certificate at %s ..." % url) - requests.post(url, data=cert_buf, - 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) + url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() + click.echo("Publishing certificate at %s ..." % url) + requests.post(url, data=cert_buf, + headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) + push.publish("request-signed", common_name.value) return cert, cert_buf diff --git a/certidude/cli.py b/certidude/cli.py index 881696c..b065460 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -728,8 +728,9 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat @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): # 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-mimeparse python-markdown python-xattr python-jinja2 python-cffi python-openssl software-properties-common") + apt("python-setproctitle cython python-dev libkrb5-dev libffi-dev libssl-dev") + 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") click.echo("Software dependencies installed") @@ -1086,10 +1087,11 @@ def certidude_cron(): @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("-l", "--listen", default="0.0.0.0", help="Listen address") @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 certidude.signer import SignServer from certidude import authority, const @@ -1177,16 +1179,13 @@ def certidude_serve(port, listen, fork): click.echo("Serving API at %s:%d" % (listen, port)) from wsgiref.simple_server import make_server, WSGIServer - from SocketServer import ForkingMixIn from certidude.api import certidude_app - class ThreadingWSGIServer(ForkingMixIn, WSGIServer): - pass click.echo("Listening on %s:%d" % (listen, port)) 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"): os.system("/etc/cron.hourly/certidude") - if config.EVENT_SOURCE_PUBLISH: - from certidude.push import EventSourceLogHandler - log_handlers.append(EventSourceLogHandler()) + from certidude.push import EventSourceLogHandler + log_handlers.append(EventSourceLogHandler()) for j in logging.Logger.manager.loggerDict.values(): 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) drop_privileges() - def quit_handler(*args, **kwargs): - click.echo("Shutting down HTTP server...") - raise KeyboardInterrupt - signal.signal(signal.SIGHUP, quit_handler) - try: - httpd.serve_forever() - except KeyboardInterrupt: - click.echo("Caught Ctrl-C, server stopped") + + class ExitResource(): + """ + Provide way to gracefully shutdown server + """ + def on_get(self, req, resp): + assert httpd._BaseServer__shutdown_request == False + 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") diff --git a/certidude/config.py b/certidude/config.py index e4345f8..2023bc7 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -73,19 +73,14 @@ LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") LOGGING_BACKEND = cp.get("logging", "backend") -if "whitelist" == AUTHORIZATION_BACKEND: - 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", "admins whitelist").split(" ") if j]) -elif "posix" == AUTHORIZATION_BACKEND: - USERS_GROUP = cp.get("authorization", "posix user group") - ADMIN_GROUP = cp.get("authorization", "posix admin group") -elif "ldap" == AUTHORIZATION_BACKEND: - LDAP_USER_FILTER = cp.get("authorization", "ldap user 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) +USERS_WHITELIST = set([j for j in cp.get("authorization", "user whitelist").split(" ") if j]) +ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admin whitelist").split(" ") if j]) +USERS_GROUP = cp.get("authorization", "posix user group") +ADMIN_GROUP = cp.get("authorization", "posix admin group") +LDAP_USER_FILTER = cp.get("authorization", "ldap user 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'") TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("tagging")] diff --git a/certidude/push.py b/certidude/push.py index e7a45b2..80d9c53 100644 --- a/certidude/push.py +++ b/certidude/push.py @@ -13,9 +13,6 @@ def publish(event_type, event_data): """ assert event_type, "No event type specified" assert event_data, "No event data specified" - if not config.EVENT_SOURCE_PUBLISH: - # Push server disabled - return if not isinstance(event_data, basestring): from certidude.decorators import MyEncoder diff --git a/certidude/templates/server/server.conf b/certidude/templates/server/server.conf index 53cf473..9742636 100644 --- a/certidude/templates/server/server.conf +++ b/certidude/templates/server/server.conf @@ -9,7 +9,7 @@ backends = pam ;backends = ldap ;backends = kerberos ldap ;backends = kerberos pam -ldap uri = ldaps://dc1.example.com +ldap uri = ldaps://dc.example.lan kerberos keytab = FILE:{{ kerberos_keytab }} [accounts] @@ -23,8 +23,8 @@ kerberos keytab = FILE:{{ kerberos_keytab }} backend = posix ;backend = ldap ldap gssapi credential cache = /run/certidude/krb5cc -ldap uri = ldap://dc1.example.com -ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=com{% endif %} +ldap uri = ldap://dc.example.lan +ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=lan{% endif %} [authorization] # The authorization backend specifies how the users are authorized. @@ -38,7 +38,11 @@ posix admin group = sudo ;backend = ldap ldap computer filter = (&(objectclass=user)(objectclass=computer)(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 user subnets = 0.0.0.0/0 diff --git a/tests/test_cli.py b/tests/test_cli.py index c3c6c0b..2ff6eaf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -93,6 +93,7 @@ def clean_server(): except OSError: pass + if os.path.exists("/var/lib/certidude/ca.example.lan"): shutil.rmtree("/var/lib/certidude/ca.example.lan") if os.path.exists("/etc/certidude/server.conf"): @@ -126,10 +127,25 @@ def clean_server(): if os.path.exists("/etc/openvpn/keys"): shutil.rmtree("/etc/openvpn/keys") - # System packages - os.system("apt purge -y nginx libnginx-mod-nchan openvpn strongswan") - os.system("apt-get -y autoremove") + # Stop samba + if os.path.exists("/run/samba/samba.pid"): + 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(): 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" + if not os.path.exists("/etc/resolv.conf.orig"): + shutil.copyfile("/etc/resolv.conf", "/etc/resolv.conf.orig") + clean_server() 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 import const @@ -156,7 +193,7 @@ def test_cli_setup_authority(): assert os.system("nginx -t") == 0, "invalid nginx configuration" 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 <= 0xfffffffffffffffffffffffffffffffffffffff assert authority.ca_cert.not_valid_before < datetime.now() @@ -182,7 +219,7 @@ def test_cli_setup_authority(): server_pid = os.fork() if not server_pid: # 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 return @@ -519,15 +556,13 @@ def test_cli_setup_authority(): # Test session API call r = client().simulate_get("/api/", headers={"Authorization":usertoken}) assert r.status_code == 200 - r = client().simulate_get("/api/", headers={"Authorization":admintoken}) assert r.status_code == 200 - r = client().simulate_get("/api/", headers={"Accept":"text/plain", "Authorization":admintoken}) assert r.status_code == 415 # invalid media type - r = client().simulate_get("/api/") assert r.status_code == 401 + assert "Please authenticate" in r.text # Test token mech @@ -568,9 +603,14 @@ def test_cli_setup_authority(): # Beyond this point don't use client() const.STORAGE_PATH = "/tmp/" + ############# ### nginx ### ############# + + # In this case nginx is set up as web server with TLS certificates + # generated by certidude. + clean_client() result = runner.invoke(cli, ["setup", "nginx", "-cn", "www", "ca.example.lan"]) @@ -612,11 +652,15 @@ def test_cli_setup_authority(): # Test nginx setup assert os.system("nginx -t") == 0, "Generated nginx config was invalid" + # TODO: test client verification with curl + ############### ### OpenVPN ### ############### + # First OpenVPN server is set up + clean_client() 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("/etc/openvpn/site-to-client.conf") - # Reset config + # Secondly OpenVPN client is set up + os.unlink("/etc/certidude/client.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 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: test client verification with curl + ############### ### 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 ### ################### @@ -783,18 +897,18 @@ def test_cli_setup_authority(): assert authority.signer_exec("exit") == "ok" # Shut down server - with open("/run/certidude/server.pid") as fh: - os.kill(int(fh.read()), 1) + requests.get("http://ca.example.lan/api/exit") + os.waitpid(server_pid, 0) # 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 len(inbox) == 0, inbox # Make sure all messages were checked - os.waitpid(server_pid, 0) - os.system("service nginx stop") os.system("service openvpn stop") os.system("ipsec stop") clean_server() + +if __name__ == "__main__": + test_cli_setup_authority()