1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

tests: Preliminary tests for Kerberos/LDAP auth

This commit is contained in:
Lauri Võsandi 2017-05-07 19:11:24 +00:00
parent 60a0f2ba7c
commit 71e77154d7
10 changed files with 228 additions and 106 deletions

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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")]

View File

@ -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

View File

@ -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

View File

@ -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()