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:
parent
60a0f2ba7c
commit
71e77154d7
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user