From cca9d2ab2dced4eb4769c558c668467dfcbd262d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Wed, 25 Jan 2017 09:43:19 +0000 Subject: [PATCH 1/2] Refactor LDAP authentication * ldap uri can be specified in /etc/certidude/server.conf now * /etc/ldap/ldap.conf is ignored --- certidude/api/__init__.py | 1 - certidude/auth.py | 49 ++++++++++++++--------- certidude/config.py | 19 ++------- certidude/templates/certidude-server.conf | 5 ++- certidude/user.py | 16 ++++---- 5 files changed, 44 insertions(+), 46 deletions(-) diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 531aa27..8cb51dd 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -44,7 +44,6 @@ class SessionResource(object): @login_required @event_source def on_get(self, req, resp): - return dict( user = dict( name=req.context.get("user").name, diff --git a/certidude/auth.py b/certidude/auth.py index 872cce3..5a2c60b 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -122,9 +122,9 @@ def authenticate(optional=False): import ldap if not req.auth: - resp.append_header("WWW-Authenticate", "Basic") - raise falcon.HTTPUnauthorized("Forbidden", - "Please authenticate with %s domain account or supply UPN" % const.DOMAIN) + raise falcon.HTTPUnauthorized("Unauthorized", + "No authentication header provided", + ("Basic",)) if not req.auth.startswith("Basic "): raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth) @@ -133,26 +133,35 @@ def authenticate(optional=False): basic, token = req.auth.split(" ", 1) user, passwd = b64decode(token).split(":", 1) - for server in config.LDAP_SERVERS: - click.echo("Connecting to %s as %s" % (server, user)) - conn = ldap.initialize(server) - conn.set_option(ldap.OPT_REFERRALS, 0) - try: - conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, const.DOMAIN), passwd) - except ldap.LDAPError, e: - resp.append_header("WWW-Authenticate", "Basic") - logger.critical(u"LDAP bind authentication failed for user %s from %s", - repr(user), req.context.get("remote_addr")) - raise falcon.HTTPUnauthorized("Forbidden", - "Please authenticate with %s domain account or supply UPN" % const.DOMAIN) + click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, user)) + conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI) + conn.set_option(ldap.OPT_REFERRALS, 0) - req.context["ldap_conn"] = conn - break - else: - raise ValueError("No LDAP servers!") + if "@" not in user: + user = "%s@%s" % (user, const.DOMAIN) + logger.debug("Expanded username to %s", user) + try: + conn.simple_bind_s(user, passwd) + except ldap.STRONG_AUTH_REQUIRED: + logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://") + raise + except ldap.SERVER_DOWN: + logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?", + config.LDAP_AUTHENTICATION_URI) + raise + except ldap.INVALID_CREDENTIALS: + logger.critical(u"LDAP bind authentication failed for user %s from %s", + repr(user), req.context.get("remote_addr")) + raise falcon.HTTPUnauthorized("Forbidden", + "Please authenticate with %s domain account or supply UPN" % const.DOMAIN, + ("Basic",)) + + req.context["ldap_conn"] = conn req.context["user"] = User.objects.get(user) - return func(resource, req, resp, *args, **kwargs) + retval = func(resource, req, resp, *args, **kwargs) + conn.unbind_s() + return retval def pam_authenticate(resource, req, resp, *args, **kwargs): diff --git a/certidude/config.py b/certidude/config.py index adfd4a8..7fed3d6 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -18,8 +18,10 @@ AUTHENTICATION_BACKENDS = set([j for j in AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, posix ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap -if ACCOUNTS_BACKEND == "ldap": - LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") +LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri") +LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") +LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") +LDAP_BASE = cp.get("accounts", "ldap base") USER_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "user subnets").split(" ") if j]) @@ -78,17 +80,4 @@ elif "ldap" == AUTHORIZATION_BACKEND: else: raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) -for line in open("/etc/ldap/ldap.conf"): - line = line.strip().lower() - if "#" in line: - line, _ = line.split("#", 1) - if not " " in line: - continue - key, value = line.split(" ", 1) - if key == "uri": - LDAP_SERVERS = set([j for j in value.split(" ") if j]) - click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS)) - elif key == "base": - LDAP_BASE = value - # TODO: Check if we don't have base or servers diff --git a/certidude/templates/certidude-server.conf b/certidude/templates/certidude-server.conf index b5da14a..7866be0 100644 --- a/certidude/templates/certidude-server.conf +++ b/certidude/templates/certidude-server.conf @@ -9,11 +9,12 @@ backends = pam ;backends = ldap ;backends = kerberos ldap ;backends = kerberos pam +ldap uri = ldaps://dc1.example.com [accounts] # The accounts backend specifies how the user's given name, surname and e-mail # address are looked up. In case of 'posix' basically 'getent passwd' is performed, -# in case of 'ldap' a search is performed on LDAP server specified in /etc/ldap/ldap.conf +# in case of 'ldap' a search is performed on LDAP server specified by ldap uri # with Kerberos credential cache initialized at path specified by environment variable KRB5CCNAME # If certidude setup authority was performed correctly the credential cache should be # updated automatically by /etc/cron.hourly/certidude @@ -21,6 +22,8 @@ backends = pam 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 %} [authorization] # The authorization backend specifies how the users are authorized. diff --git a/certidude/user.py b/certidude/user.py index d31198b..23996ca 100644 --- a/certidude/user.py +++ b/certidude/user.py @@ -70,17 +70,15 @@ class DirectoryConnection(object): raise ValueError("Ticket cache at %s not initialized, unable to " "authenticate with computer account against LDAP server!" % config.LDAP_GSSAPI_CRED_CACHE) os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE - for server in config.LDAP_SERVERS: - self.conn = ldap.initialize(server) - self.conn.set_option(ldap.OPT_REFERRALS, 0) - click.echo("Connecing to %s using Kerberos ticket cache from %s" % - (server, config.LDAP_GSSAPI_CRED_CACHE)) - self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) - return self.conn - raise ValueError("No LDAP servers specified!") + self.conn = ldap.initialize(config.LDAP_ACCOUNTS_URI) + self.conn.set_option(ldap.OPT_REFERRALS, 0) + click.echo("Connecing to %s using Kerberos ticket cache from %s" % + (config.LDAP_ACCOUNTS_URI, config.LDAP_GSSAPI_CRED_CACHE)) + self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) + return self.conn def __exit__(self, type, value, traceback): - self.conn.unbind_s + self.conn.unbind_s() class ActiveDirectoryUserManager(object): From 1925207a6da5ebd8a58c3ff877e50851cf93db03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Wed, 25 Jan 2017 11:34:08 +0000 Subject: [PATCH 2/2] Add OpenVPN bundle generation --- certidude/api/bundle.py | 19 ++++++++---- certidude/authority.py | 35 ++++++++++++++++++++++- certidude/config.py | 3 ++ certidude/templates/certidude-server.conf | 5 ++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/certidude/api/bundle.py b/certidude/api/bundle.py index 1cc6243..0249640 100644 --- a/certidude/api/bundle.py +++ b/certidude/api/bundle.py @@ -12,6 +12,8 @@ KEYWORDS = ( (u"iPhone", u"iphone"), (u"iPad", u"ipad"), (u"Ubuntu", u"ubuntu"), + (u"Fedora", u"fedora"), + (u"Linux", u"linux"), ) class BundleResource(object): @@ -29,8 +31,15 @@ class BundleResource(object): hashlib.sha256(req.user_agent).hexdigest()[:8]) logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user")) - resp.set_header("Content-Type", "application/x-pkcs12") - resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii")) - resp.body, cert = authority.generate_pkcs12_bundle(common_name, - owner=req.context.get("user")) - + if config.BUNDLE_FORMAT == "p12": + resp.set_header("Content-Type", "application/x-pkcs12") + resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii")) + resp.body, cert = authority.generate_pkcs12_bundle(common_name, + owner=req.context.get("user")) + elif config.BUNDLE_FORMAT == "ovpn": + resp.set_header("Content-Type", "application/x-openvpn") + resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii")) + resp.body, cert = authority.generate_ovpn_bundle(common_name, + owner=req.context.get("user")) + else: + raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT) diff --git a/certidude/authority.py b/certidude/authority.py index 237ef8a..aea94f6 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -14,6 +14,7 @@ from cryptography.hazmat.primitives import hashes, serialization from certidude import config, push, mailer, const from certidude.wrappers import Certificate, Request from certidude import errors +from jinja2 import Template RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" @@ -21,7 +22,6 @@ RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z # https://jamielinux.com/docs/openssl-certificate-authority/ # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py - # Cache CA certificate certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH)) @@ -185,6 +185,39 @@ def delete_request(common_name): requests.delete(config.PUSH_PUBLISH % request.fingerprint(), headers={"User-Agent": "Certidude API"}) +def generate_ovpn_bundle(common_name, owner=None): + # Construct private key + click.echo("Generating 4096-bit RSA key...") + + key = rsa.generate_private_key( + public_exponent=65537, + key_size=4096, + backend=default_backend() + ) + + csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ + x509.NameAttribute(k, v) for k, v in ( + (NameOID.COMMON_NAME, common_name), + (NameOID.GIVEN_NAME, owner and owner.given_name), + (NameOID.SURNAME, owner and owner.surname), + ) if v + ])) + + # Sign CSR + cert = sign(Request( + csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True) + + bundle = Template(open(config.OPENVPN_BUNDLE_TEMPLATE).read()).render( + ca = certificate.dump(), + key = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ), + cert = cert.dump(), + crl=export_crl(), + ) + return bundle, cert def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): """ diff --git a/certidude/config.py b/certidude/config.py index 7fed3d6..3da5af7 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -40,6 +40,9 @@ SIGNED_DIR = cp.get("authority", "signed dir") REVOKED_DIR = cp.get("authority", "revoked dir") OUTBOX = cp.get("authority", "outbox") +BUNDLE_FORMAT = cp.get("authority", "bundle format") +OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template") + USER_CERTIFICATE_ENROLLMENT = { "forbidden": False, "single allowed": True, "multiple allowed": True }[ cp.get("authority", "user certificate enrollment")] diff --git a/certidude/templates/certidude-server.conf b/certidude/templates/certidude-server.conf index 7866be0..8b43a4c 100644 --- a/certidude/templates/certidude-server.conf +++ b/certidude/templates/certidude-server.conf @@ -95,3 +95,8 @@ revoked dir = {{ directory }}/revoked/ expired dir = {{ directory }}/expired/ outbox = {{ outbox }} +bundle format = p12 +;bundle format = ovpn + +openvpn bundle template = /etc/certidude/template.ovpn +