mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-30 17:09:19 +00:00 
			
		
		
		
	Refactor users, add OpenVPN and mailing support
* Add abstraction for user objects * Mail authority admins about pending, revoked and signed certificates * Add NetworkManager's OpenVPN plugin support * Improve CRL support * Refactor CSRF protection * Update documentation
This commit is contained in:
		
							
								
								
									
										117
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								README.rst
									
									
									
									
									
								
							| @@ -17,8 +17,9 @@ eventually support PKCS#11 and in far future WebCrypto. | ||||
|  | ||||
| .. figure:: doc/usecase-diagram.png | ||||
|  | ||||
| Certidude is mainly designed for VPN gateway operators to make VPN adoption usage | ||||
| as simple as possible. | ||||
| Certidude is mainly designed for VPN gateway operators to make | ||||
| desktop/laptop VPN setup as easy as possible. | ||||
| User certificate management eg. for HTTPS is also made reasonably simple. | ||||
| For a full-blown CA you might want to take a look at | ||||
| `EJBCA <http://www.ejbca.org/features.html>`_ or | ||||
| `OpenCA <https://pki.openca.org/>`_. | ||||
| @@ -27,24 +28,29 @@ For a full-blown CA you might want to take a look at | ||||
| Features | ||||
| -------- | ||||
|  | ||||
| Common: | ||||
|  | ||||
| * Standard request, sign, revoke workflow via web interface. | ||||
| * Colored command-line interface, check out ``certidude list``. | ||||
| * OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``. | ||||
| * strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``. | ||||
| * Kerberos and basic auth based web interface authentication. | ||||
| * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind. | ||||
| * POSIX groups and Active Directory (LDAP) group membership based authorization. | ||||
| * Command-line interface, check out ``certidude list``. | ||||
| * Privilege isolation, separate signer process is spawned per private key isolating | ||||
|   private key use from the the web interface. | ||||
| * Certificate numbering obfuscation, certificate serial numbers are intentionally | ||||
|   randomized to avoid leaking information about business practices. | ||||
| * Server-side events support via for example nginx-push-stream-module. | ||||
| * Kerberos based web interface authentication. | ||||
| * File based whitelist authorization, easy to integrate with LDAP as shown below. | ||||
| * Certificate serial numbers are intentionally randomized to avoid leaking information about business practices. | ||||
| * Server-side events support via `nchan <https://nchan.slact.net/>`_. | ||||
| * E-mail notifications about pending, signed and revoked certificates. | ||||
|  | ||||
| Virtual private networking: | ||||
|  | ||||
| Coming soon | ||||
| ----------- | ||||
| * OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``. | ||||
| * strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``. | ||||
| * NetworkManager integration, check out ``certidude setup openvpn networkmanager`` and ``certidude setup strongswan networkmanager``. | ||||
|  | ||||
| * Refactor mailing subsystem and server-side events to use hooks. | ||||
| * Notifications via e-mail. | ||||
| HTTPS: | ||||
|  | ||||
| * P12 bundle generation for web browsers, seems to work well with Android | ||||
| * HTTPS server setup with client verification, check out ``certidude setup nginx`` | ||||
|  | ||||
|  | ||||
| TODO | ||||
| @@ -60,6 +66,7 @@ TODO | ||||
| * Cronjob for deleting expired certificates | ||||
| * Signer process logging. | ||||
|  | ||||
|  | ||||
| Install | ||||
| ------- | ||||
|  | ||||
| @@ -67,15 +74,15 @@ To install Certidude: | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
|     apt-get install -y python python-pip python-dev cython \ | ||||
|     apt-get install -y python python-pip python-dev cython python-configparser \ | ||||
|         python-pysqlite2 python-mysql.connector python-ldap \ | ||||
|         build-essential libffi-dev libssl-dev libkrb5-dev \ | ||||
|         ldap-utils krb5-user default-mta \ | ||||
|         libsasl2-modules-gssapi-mit | ||||
|     pip3 install certidude | ||||
|  | ||||
| Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI, | ||||
| not the outdated ones provided by APT. | ||||
| Make sure you're running PyOpenSSL 0.15+ from PyPI, | ||||
| not the outdated one provided by APT. | ||||
|  | ||||
| Create a system user for ``certidude``: | ||||
|  | ||||
| @@ -85,10 +92,10 @@ Create a system user for ``certidude``: | ||||
|     mkdir /etc/certidude | ||||
|  | ||||
|  | ||||
| Setting up CA | ||||
| -------------- | ||||
| Setting up authority | ||||
| -------------------- | ||||
|  | ||||
| First make sure the machine used for CA has fully qualified | ||||
| First make sure the machine used for certificate authority has fully qualified | ||||
| domain name set up properly. | ||||
| You can check it with: | ||||
|  | ||||
| @@ -96,10 +103,10 @@ You can check it with: | ||||
|  | ||||
|     hostname -f | ||||
|  | ||||
| The command should return ca.example.co | ||||
| The command should return ca.example.com | ||||
|  | ||||
| Certidude can set up CA relatively easily, following will set up | ||||
| CA in /var/lib/certidude/hostname.domain.tld: | ||||
| Certidude can set up certificate authority relatively easily, | ||||
| following will set up certificate authority in /var/lib/certidude/hostname.domain.tld: | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
| @@ -176,6 +183,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl | ||||
|     env = LANG=C.UTF-8 | ||||
|     env = LC_ALL=C.UTF-8 | ||||
|     env = KRB5_KTNAME=/etc/certidude/server.keytab | ||||
|     env = KRB5CCNAME=/run/certidude/krb5cc | ||||
|  | ||||
| Also enable the application: | ||||
|  | ||||
| @@ -183,7 +191,7 @@ Also enable the application: | ||||
|  | ||||
|     ln -s ../apps-available/certidude.ini /etc/uwsgi/apps-enabled/certidude.ini | ||||
|  | ||||
| We support `nginx-push-stream-module <https://github.com/wandenberg/nginx-push-stream-module>`_, | ||||
| We support `nchan <https://nchan.slact.net/>`_, | ||||
| configure the site in /etc/nginx/sites-available/certidude: | ||||
|  | ||||
| .. code:: | ||||
| @@ -238,11 +246,9 @@ Also adjust ``/etc/nginx/nginx.conf``: | ||||
|  | ||||
|     events { | ||||
|         worker_connections 768; | ||||
|         # multi_accept on; | ||||
|     } | ||||
|  | ||||
|     http { | ||||
|         push_stream_shared_memory_size 32M; | ||||
|         sendfile on; | ||||
|         tcp_nopush on; | ||||
|         tcp_nodelay on; | ||||
| @@ -254,10 +260,12 @@ Also adjust ``/etc/nginx/nginx.conf``: | ||||
|         error_log /var/log/nginx/error.log; | ||||
|         gzip on; | ||||
|         gzip_disable "msie6"; | ||||
|         include /etc/nginx/conf.d/*; | ||||
|         include /etc/nginx/sites-enabled/*; | ||||
|     } | ||||
|  | ||||
| In your CA ssl.cnf make sure Certidude is aware of your nginx setup: | ||||
| In your Certidude server's /etc/certidude/server.conf make sure Certidude | ||||
| is aware of your nginx setup: | ||||
|  | ||||
| .. code:: | ||||
|  | ||||
| @@ -309,8 +317,6 @@ Reset Kerberos configuration in ``/etc/krb5.conf``: | ||||
|     default_realm = EXAMPLE.LAN | ||||
|     dns_lookup_realm = true | ||||
|     dns_lookup_kdc = true | ||||
|     forwardable = true | ||||
|     proxiable = true | ||||
|  | ||||
| Initialize Kerberos credentials: | ||||
|  | ||||
| @@ -332,43 +338,25 @@ Set up Kerberos keytab for the web service: | ||||
|     chown root:certidude /etc/certidude/server.keytab | ||||
|     chmod 640 /etc/certidude/server.keytab | ||||
|  | ||||
| Reconfigure /etc/certidude/server.conf: | ||||
|  | ||||
| Setting up authorization | ||||
| ------------------------ | ||||
| .. code:: ini | ||||
|  | ||||
| Obviously arbitrary Kerberos authenticated user should not have access to | ||||
| the CA web interface. | ||||
| You could either specify user name list | ||||
| in ``/etc/ssl/openssl.cnf``: | ||||
|     [authentication] | ||||
|     backends = kerberos | ||||
|  | ||||
| .. code:: bash | ||||
|     [authorization] | ||||
|     backend = ldap | ||||
|     ldap gssapi credential cache = /run/certidude/krb5cc | ||||
|     ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s)) | ||||
|     ldap admin filter = (&(objectclass=user)(objectclass=person)(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=com)(samaccountname=%s)) | ||||
|  | ||||
|     admin_users=alice bob john kate | ||||
|  | ||||
| Or alternatively specify file path: | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
|     admin_users=/run/certidude/user.whitelist | ||||
|  | ||||
| Use following shell snippets eg in ``/etc/cron.hourly/update-certidude-user-whitelist`` | ||||
| to generate user whitelist via LDAP: | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
|     ldapsearch -H ldap://dc1.example.com -s sub -x -LLL \ | ||||
|         -D 'cn=certidude,cn=Users,dc=example,dc=com' \ | ||||
|         -w 'certidudepass' \ | ||||
|         -b 'dc=example,dc=com' \ | ||||
|         '(&(objectClass=user)(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=com))' sAMAccountName userPrincipalName givenName sn \ | ||||
|     | python3 -c "import ldif3; import sys; [sys.stdout.write('%s:%s:%s:%s\n' % (a.pop('sAMAccountName')[0], a.pop('userPrincipalName')[0], a.pop('givenName')[0], a.pop('sn')[0])) for _, a in ldif3.LDIFParser(sys.stdin.buffer).parse()]" \ | ||||
|     > /run/certidude/user.whitelist | ||||
|  | ||||
| Set permissions: | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
|     chmod 700 /etc/cron.hourly/update-certidude-user-whitelist | ||||
| User filter here specified which users can log in to Certidude web interface | ||||
| at all eg. for generating user certificates for HTTPS. | ||||
| Admin filter specifies which users are allowed to sign and revoke certificates. | ||||
| Adjust admin filter according to your setup. | ||||
| Also make sure there is cron.hourly job for creating GSSAPI credential cache - | ||||
| that's necessary for querying LDAP using Certidude machine's credentials. | ||||
|  | ||||
|  | ||||
| Automating certificate setup | ||||
| @@ -384,7 +372,7 @@ Create ``/etc/NetworkManager/dispatcher.d/certidude`` with following content: | ||||
|  | ||||
|     case "$2" in | ||||
|         up) | ||||
|             LANG=C.UTF-8 /usr/local/bin/certidude setup strongswan networkmanager ca.example.com gateway.example.com | ||||
|             LANG=C.UTF-8 /usr/local/bin/certidude request spawn -k | ||||
|         ;; | ||||
|     esac | ||||
|  | ||||
| @@ -397,8 +385,7 @@ Finally make it executable: | ||||
| Whenever a wired or wireless connection is brought up, | ||||
| the dispatcher invokes ``certidude`` in order to generate RSA keys, | ||||
| submit CSR, fetch signed certificate, | ||||
| create NetworkManager configuration for the VPN connection and | ||||
| finally to bring up the VPN tunnel as well. | ||||
| create NetworkManager configuration for the VPN connection. | ||||
|  | ||||
|  | ||||
| Development | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from datetime import datetime | ||||
| from time import sleep | ||||
| from certidude import authority, mailer | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.user import User | ||||
| from certidude.decorators import serialize, event_source, csrf_protection | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from certidude import constants, config | ||||
| @@ -33,34 +34,16 @@ class CertificateAuthorityResource(object): | ||||
|         logger.info("Served CA certificate to %s", req.context.get("remote_addr")) | ||||
|         resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") | ||||
|         resp.append_header("Content-Type", "application/x-x509-ca-cert") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=ca.crt") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % | ||||
|             constants.HOSTNAME.encode("ascii")) | ||||
|  | ||||
|  | ||||
| class SessionResource(object): | ||||
|     @csrf_protection | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     @event_source | ||||
|     def on_get(self, req, resp): | ||||
|         if config.ACCOUNTS_BACKEND == "ldap": | ||||
|             import ldap | ||||
|             ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*") | ||||
|             r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, | ||||
|                     ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"]) | ||||
|  | ||||
|             for dn,entry in r: | ||||
|                 cn, = entry.get("cn") | ||||
|                 break | ||||
|             else: | ||||
|                 raise ValueError("Failed to look up group %s in LDAP" % repr(group_name)) | ||||
|  | ||||
|             admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")]) | ||||
|         elif config.ACCOUNTS_BACKEND == "posix": | ||||
|             import grp | ||||
|             _, _, gid, members = grp.getgrnam(config.ADMINS_GROUP) | ||||
|             admins = dict([(j, j) for j in members]) | ||||
|         else: | ||||
|             raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) | ||||
|  | ||||
|         return dict( | ||||
|             user = dict( | ||||
| @@ -72,12 +55,6 @@ class SessionResource(object): | ||||
|             request_submission_allowed = sum( # Dirty hack! | ||||
|                 [req.context.get("remote_addr") in j | ||||
|                     for j in config.REQUEST_SUBNETS]), | ||||
|             user_subnets = config.USER_SUBNETS, | ||||
|             autosign_subnets = config.AUTOSIGN_SUBNETS, | ||||
|             request_subnets = config.REQUEST_SUBNETS, | ||||
|             admin_subnets=config.ADMIN_SUBNETS, | ||||
|             admin_users = admins, | ||||
|             #admin_users=config.ADMIN_USERS, | ||||
|             authority = dict( | ||||
|                 outbox = config.OUTBOX, | ||||
|                 certificate = authority.certificate, | ||||
| @@ -85,7 +62,12 @@ class SessionResource(object): | ||||
|                 requests=authority.list_requests(), | ||||
|                 signed=authority.list_signed(), | ||||
|                 revoked=authority.list_revoked(), | ||||
|             ) if config.ADMINS_GROUP in req.context.get("groups") else None, | ||||
|                 admin_users = User.objects.filter_admins(), | ||||
|                 user_subnets = config.USER_SUBNETS, | ||||
|                 autosign_subnets = config.AUTOSIGN_SUBNETS, | ||||
|                 request_subnets = config.REQUEST_SUBNETS, | ||||
|                 admin_subnets=config.ADMIN_SUBNETS, | ||||
|             ) if req.context.get("user").is_admin() else None, | ||||
|             features=dict( | ||||
|                 tagging=config.TAGGING_BACKEND, | ||||
|                 leases=False, #config.LEASES_BACKEND, | ||||
| @@ -124,7 +106,7 @@ class BundleResource(object): | ||||
|         common_name = req.context["user"].mail | ||||
|         logger.info("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) | ||||
|         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")) | ||||
|  | ||||
| @@ -132,7 +114,6 @@ class BundleResource(object): | ||||
| import ipaddress | ||||
|  | ||||
| class NormalizeMiddleware(object): | ||||
|     @csrf_protection | ||||
|     def process_request(self, req, resp, *args): | ||||
|         assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" | ||||
|         req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import ipaddress | ||||
| import os | ||||
| from certidude import config, authority, helpers, push, errors | ||||
| from certidude.auth import login_required, login_optional, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
| from certidude.decorators import serialize, csrf_protection | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from certidude.firewall import whitelist_subnets, whitelist_content_types | ||||
|  | ||||
| @@ -19,6 +19,7 @@ class RequestListResource(object): | ||||
|     def on_get(self, req, resp): | ||||
|         return authority.list_requests() | ||||
|  | ||||
|  | ||||
|     @login_optional | ||||
|     @whitelist_subnets(config.REQUEST_SUBNETS) | ||||
|     @whitelist_content_types("application/pkcs10") | ||||
| @@ -53,7 +54,7 @@ class RequestListResource(object): | ||||
|         # Process automatic signing if the IP address is whitelisted and autosigning was requested | ||||
|         if req.get_param_as_bool("autosign"): | ||||
|             for subnet in config.AUTOSIGN_SUBNETS: | ||||
|                 if subnet.overlaps(req.context.get("remote_addr")): | ||||
|                 if req.context.get("remote_addr") in subnet: | ||||
|                     try: | ||||
|                         resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||
|                         resp.body = authority.sign(csr).dump() | ||||
| @@ -103,6 +104,8 @@ class RequestDetailResource(object): | ||||
|             csr.common_name, req.context.get("remote_addr")) | ||||
|         return csr | ||||
|  | ||||
|  | ||||
|     @csrf_protection | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_patch(self, req, resp, cn): | ||||
| @@ -118,6 +121,8 @@ class RequestDetailResource(object): | ||||
|         logger.info("Signing request %s signed by %s from %s", csr.common_name, | ||||
|             req.context.get("user"), req.context.get("remote_addr")) | ||||
|  | ||||
|  | ||||
|     @csrf_protection | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, cn): | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import falcon | ||||
| import logging | ||||
| from certidude import authority | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
| from certidude.decorators import serialize, csrf_protection | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
|  | ||||
| @@ -24,20 +24,21 @@ class SignedCertificateDetailResource(object): | ||||
|         try: | ||||
|             cert = authority.get_signed(cn) | ||||
|         except EnvironmentError: | ||||
|             logger.warning("Failed to serve non-existant certificate %s to %s", | ||||
|             logger.warning(u"Failed to serve non-existant certificate %s to %s", | ||||
|                 cn, req.context.get("remote_addr")) | ||||
|             resp.body = "No certificate CN=%s found" % cn | ||||
|             raise falcon.HTTPNotFound() | ||||
|         else: | ||||
|             logger.debug("Served certificate %s to %s", | ||||
|             logger.debug(u"Served certificate %s to %s", | ||||
|                 cn, req.context.get("remote_addr")) | ||||
|             return cert | ||||
|  | ||||
|  | ||||
|     @csrf_protection | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         logger.info("Revoked certificate %s by %s from %s", | ||||
|         logger.info(u"Revoked certificate %s by %s from %s", | ||||
|             cn, req.context.get("user"), req.context.get("remote_addr")) | ||||
|         authority.revoke_certificate(cn) | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import falcon | ||||
| import logging | ||||
| from certidude.relational import RelationalMixin | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
| from certidude.decorators import serialize, csrf_protection | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
|  | ||||
| @@ -17,6 +17,7 @@ class TagResource(RelationalMixin): | ||||
|         return self.iterfetch("select * from tag") | ||||
|  | ||||
|  | ||||
|     @csrf_protection | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
| @@ -51,6 +52,7 @@ class TagDetailResource(RelationalMixin): | ||||
|         raise falcon.HTTPNotFound() | ||||
|  | ||||
|  | ||||
|     @csrf_protection | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
| @@ -63,6 +65,7 @@ class TagDetailResource(RelationalMixin): | ||||
|         push.publish("tag-updated", identifier) | ||||
|  | ||||
|  | ||||
|     @csrf_protection | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|   | ||||
| @@ -6,14 +6,13 @@ import logging | ||||
| import os | ||||
| import re | ||||
| import socket | ||||
| from certidude.user import User | ||||
| from certidude.firewall import whitelist_subnets | ||||
| from certidude import config, constants | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
|  | ||||
| FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] | ||||
|  | ||||
| if config.AUTHENTICATION_BACKENDS == {"kerberos"}: | ||||
| if "kerberos" in config.AUTHENTICATION_BACKENDS: | ||||
|     ktname = os.getenv("KRB5_KTNAME") | ||||
|  | ||||
|     if not ktname: | ||||
| @@ -24,139 +23,13 @@ if config.AUTHENTICATION_BACKENDS == {"kerberos"}: | ||||
|         exit(248) | ||||
|  | ||||
|     try: | ||||
|         principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) | ||||
|         principal = kerberos.getServerPrincipalDetails("HTTP", constants.FQDN) | ||||
|     except kerberos.KrbError as exc: | ||||
|         click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True) | ||||
|         click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % ( | ||||
|             constants.FQDN, exc), err=True) | ||||
|         exit(249) | ||||
|     else: | ||||
|         click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) | ||||
|  | ||||
|  | ||||
| class User(object): | ||||
|     def __init__(self, name): | ||||
|         if "@" in name: | ||||
|             self.mail = name | ||||
|             self.name, self.domain = name.split("@") | ||||
|         else: | ||||
|             self.mail = None | ||||
|             self.name, self.domain = name, None | ||||
|         self.given_name, self.surname = None, None | ||||
|  | ||||
|     def __repr__(self): | ||||
|         if self.given_name and self.surname: | ||||
|             return u"%s %s <%s>" % (self.given_name, self.surname, self.mail) | ||||
|         else: | ||||
|             return self.mail | ||||
|  | ||||
|  | ||||
| def member_of(group_name): | ||||
|     """ | ||||
|     Check if requesting user is member of an UNIX group | ||||
|     """ | ||||
|  | ||||
|     def wrapper(func): | ||||
|         def posix_check_group_membership(resource, req, resp, *args, **kwargs): | ||||
|             import grp | ||||
|             _, _, gid, members = grp.getgrnam(group_name) | ||||
|             if req.context.get("user").name not in members: | ||||
|                 logger.info("User '%s' not member of group '%s'", req.context.get("user").name, group_name) | ||||
|                 raise falcon.HTTPForbidden("Forbidden", "User not member of designated group") | ||||
|             req.context.get("groups").add(group_name) | ||||
|             return func(resource, req, resp, *args, **kwargs) | ||||
|  | ||||
|         def ldap_check_group_membership(resource, req, resp, *args, **kwargs): | ||||
|             import ldap | ||||
|  | ||||
|             ft = config.LDAP_MEMBERS_FILTER % (group_name, req.context.get("user").dn) | ||||
|             r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, | ||||
|                 ft.encode("utf-8"), | ||||
|                 ["member"]) | ||||
|  | ||||
|             for dn,entry in r: | ||||
|                 if not dn: continue | ||||
|                 logger.debug("User %s is member of group %s" % ( | ||||
|                     req.context.get("user"), repr(group_name))) | ||||
|                 req.context.get("groups").add(group_name) | ||||
|                 break | ||||
|             else: | ||||
|                 raise ValueError("Failed to look up group '%s' with '%s' listed as member in LDAP" % (group_name, req.context.get("user").name)) | ||||
|  | ||||
|             return func(resource, req, resp, *args, **kwargs) | ||||
|  | ||||
|         if config.AUTHORIZATION_BACKEND == "ldap": | ||||
|             return ldap_check_group_membership | ||||
|         elif config.AUTHORIZATION_BACKEND == "posix": | ||||
|             return posix_check_group_membership | ||||
|         else: | ||||
|             raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) | ||||
|     return wrapper | ||||
|  | ||||
|  | ||||
| def account_info(func): | ||||
|     # TODO: Use Privilege Account Certificate for Kerberos | ||||
|  | ||||
|     def posix_account_info(resource, req, resp, *args, **kwargs): | ||||
|         import pwd | ||||
|         _, _, _, _, gecos, _, _ = pwd.getpwnam(req.context["user"].name) | ||||
|         gecos = gecos.decode("utf-8").split(",") | ||||
|         full_name = gecos[0] | ||||
|         if full_name and " " in full_name: | ||||
|             req.context["user"].given_name, req.context["user"].surname = full_name.split(" ", 1) | ||||
|         req.context["user"].mail = req.context["user"].name + "@" + constants.DOMAIN | ||||
|         return func(resource, req, resp, *args, **kwargs) | ||||
|  | ||||
|     def ldap_account_info(resource, req, resp, *args, **kwargs): | ||||
|         import ldap | ||||
|         import ldap.sasl | ||||
|  | ||||
|         if "ldap_conn" not in req.context: | ||||
|             for server in config.LDAP_SERVERS: | ||||
|                 conn = ldap.initialize(server) | ||||
|                 conn.set_option(ldap.OPT_REFERRALS, 0) | ||||
|                 if os.path.exists("/etc/krb5.keytab"): | ||||
|                     ticket_cache = os.getenv("KRB5CCNAME") | ||||
|                     if not ticket_cache: | ||||
|                         raise ValueError("Ticket cache not initialized, unable to authenticate with computer account against LDAP server!") | ||||
|                     click.echo("Connecing to %s using Kerberos ticket cache from %s" % (server, ticket_cache)) | ||||
|                     conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) | ||||
|                 else: | ||||
|                     raise NotImplementedError("LDAP simple bind not supported, use Kerberos") | ||||
|                 req.context["ldap_conn"] = conn | ||||
|                 break | ||||
|             else: | ||||
|                 raise ValueError("No LDAP servers!") | ||||
|  | ||||
|         ft = config.LDAP_USER_FILTER % req.context.get("user").name | ||||
|         r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, | ||||
|             ft, | ||||
|             ["cn", "givenname", "sn", "mail", "userPrincipalName"]) | ||||
|  | ||||
|         for dn, entry in r: | ||||
|             if not dn: continue | ||||
|             if entry.get("givenname") and entry.get("sn"): | ||||
|                 given_name, = entry.get("givenName") | ||||
|                 surname, = entry.get("sn") | ||||
|                 req.context["user"].given_name = given_name.decode("utf-8") | ||||
|                 req.context["user"].surname = surname.decode("utf-8") | ||||
|             else: | ||||
|                 cn, = entry.get("cn") | ||||
|                 if " " in cn: | ||||
|                     req.context["user"].given_name, req.context["user"].surname = cn.decode("utf-8").split(" ", 1) | ||||
|  | ||||
|             req.context["user"].dn = dn.decode("utf-8") | ||||
|             req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,) | ||||
|             retval = func(resource, req, resp, *args, **kwargs) | ||||
|             req.context.get("ldap_conn").unbind_s() | ||||
|             return retval | ||||
|         else: | ||||
|             raise ValueError("Failed to look up %s in LDAP" % req.context.get("user")) | ||||
|  | ||||
|     if config.ACCOUNTS_BACKEND == "ldap": | ||||
|         return ldap_account_info | ||||
|     elif config.ACCOUNTS_BACKEND == "posix": | ||||
|         return posix_account_info | ||||
|     else: | ||||
|         raise NotImplementedError("Accounts backend %s not supported" % config.ACCOUNTS_BACKEND) | ||||
|         click.echo("Kerberos enabled, service principal is HTTP/%s" % constants.FQDN) | ||||
|  | ||||
|  | ||||
| def authenticate(optional=False): | ||||
| @@ -167,7 +40,7 @@ def authenticate(optional=False): | ||||
|  | ||||
|             if not req.auth: | ||||
|                 resp.append_header("WWW-Authenticate", "Negotiate") | ||||
|                 logger.debug("No Kerberos ticket offered while attempting to access %s from %s", | ||||
|                 logger.debug(u"No Kerberos ticket offered while attempting to access %s from %s", | ||||
|                     req.env["PATH_INFO"], req.context.get("remote_addr")) | ||||
|                 raise falcon.HTTPUnauthorized("Unauthorized", | ||||
|                     "No Kerberos ticket offered, are you sure you've logged in with domain user account?") | ||||
| @@ -175,7 +48,7 @@ def authenticate(optional=False): | ||||
|             token = ''.join(req.auth.split()[1:]) | ||||
|  | ||||
|             try: | ||||
|                 result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) | ||||
|                 result, context = kerberos.authGSSServerInit("HTTP@" + constants.FQDN) | ||||
|             except kerberos.GSSError as ex: | ||||
|                 # TODO: logger.error | ||||
|                 raise falcon.HTTPForbidden("Forbidden", | ||||
| @@ -185,34 +58,46 @@ def authenticate(optional=False): | ||||
|                 result = kerberos.authGSSServerStep(context, token) | ||||
|             except kerberos.GSSError as ex: | ||||
|                 kerberos.authGSSServerClean(context) | ||||
|                 # TODO: logger.error | ||||
|                 logger.error(u"Kerberos authentication failed from %s. " | ||||
|                     "Bad credentials: %s (%d)", | ||||
|                     req.context.get("remote_addr"), | ||||
|                     ex.args[0][0], ex.args[0][1]) | ||||
|                 raise falcon.HTTPForbidden("Forbidden", | ||||
|                     "Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1])) | ||||
|             except kerberos.KrbError as ex: | ||||
|                 kerberos.authGSSServerClean(context) | ||||
|                 # TODO: logger.error | ||||
|                 logger.error(u"Kerberos authentication failed from  %s. " | ||||
|                     "Bad credentials: %s (%d)", | ||||
|                     req.context.get("remote_addr"), | ||||
|                     ex.args[0][0], ex.args[0][1]) | ||||
|                 raise falcon.HTTPForbidden("Forbidden", | ||||
|                     "Bad credentials: %s" % (ex.args[0],)) | ||||
|  | ||||
|             user = kerberos.authGSSServerUserName(context) | ||||
|             req.context["user"] = User(user) | ||||
|             req.context["groups"] = set() | ||||
|             req.context["user"] = User.objects.get(user) | ||||
|  | ||||
|             try: | ||||
|                 kerberos.authGSSServerClean(context) | ||||
|             except kerberos.GSSError as ex: | ||||
|                 # TODO: logger.error | ||||
|                 logger.error(u"Kerberos authentication failed for user %s from  %s. " | ||||
|                     "Authentication system failure: %s (%d)", | ||||
|                     user, req.context.get("remote_addr"), | ||||
|                     ex.args[0][0], ex.args[0][1]) | ||||
|                 raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0])) | ||||
|  | ||||
|             if result == kerberos.AUTH_GSS_COMPLETE: | ||||
|                 logger.debug("Succesfully authenticated user %s for %s from %s", | ||||
|                 logger.debug(u"Succesfully authenticated user %s for %s from %s", | ||||
|                     req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"]) | ||||
|                 return account_info(func)(resource, req, resp, *args, **kwargs) | ||||
|                 return func(resource, req, resp, *args, **kwargs) | ||||
|             elif result == kerberos.AUTH_GSS_CONTINUE: | ||||
|                 # TODO: logger.error | ||||
|                 logger.error(u"Kerberos authentication failed for user %s from  %s. " | ||||
|                     "Unauthorized, tried GSSAPI.", | ||||
|                     user, req.context.get("remote_addr")) | ||||
|                 raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") | ||||
|             else: | ||||
|                 # TODO: logger.error | ||||
|                 logger.error(u"Kerberos authentication failed for user %s from  %s. " | ||||
|                     "Forbidden, tried GSSAPI.", | ||||
|                     user, req.context.get("remote_addr")) | ||||
|                 raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") | ||||
|  | ||||
|  | ||||
| @@ -238,27 +123,26 @@ def authenticate(optional=False): | ||||
|             basic, token = req.auth.split(" ", 1) | ||||
|             user, passwd = b64decode(token).split(":", 1) | ||||
|  | ||||
|             if "ldap_conn" not in req.context: | ||||
|                 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, constants.DOMAIN), passwd) | ||||
|                     except ldap.LDAPError, e: | ||||
|                         resp.append_header("WWW-Authenticate", "Basic") | ||||
|                         logger.debug("Failed to authenticate with user '%s'", user) | ||||
|                         raise falcon.HTTPUnauthorized("Forbidden", | ||||
|                             "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN) | ||||
|             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, constants.DOMAIN), passwd) | ||||
|                 except ldap.LDAPError, e: | ||||
|                     resp.append_header("WWW-Authenticate", "Basic") | ||||
|                     logger.critical("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" % constants.DOMAIN) | ||||
|  | ||||
|                     req.context["ldap_conn"] = conn | ||||
|                     break | ||||
|                 else: | ||||
|                     raise ValueError("No LDAP servers!") | ||||
|                 req.context["ldap_conn"] = conn | ||||
|                 break | ||||
|             else: | ||||
|                 raise ValueError("No LDAP servers!") | ||||
|  | ||||
|             req.context["user"] = User(user) | ||||
|             req.context["groups"] = set() | ||||
|             return account_info(func)(resource, req, resp, *args, **kwargs) | ||||
|             req.context["user"] = User.objects.get(user) | ||||
|             return func(resource, req, resp, *args, **kwargs) | ||||
|  | ||||
|  | ||||
|         def pam_authenticate(resource, req, resp, *args, **kwargs): | ||||
| @@ -282,11 +166,12 @@ def authenticate(optional=False): | ||||
|  | ||||
|             import simplepam | ||||
|             if not simplepam.authenticate(user, passwd, "sshd"): | ||||
|                 logger.critical("Basic authentication failed for user %s from  %s", | ||||
|                     repr(user), req.context.get("remote_addr")) | ||||
|                 raise falcon.HTTPUnauthorized("Forbidden", "Invalid password") | ||||
|  | ||||
|             req.context["user"] = User(user) | ||||
|             req.context["groups"] = set() | ||||
|             return account_info(func)(resource, req, resp, *args, **kwargs) | ||||
|             req.context["user"] = User.objects.get(user) | ||||
|             return func(resource, req, resp, *args, **kwargs) | ||||
|  | ||||
|         if config.AUTHENTICATION_BACKENDS == {"kerberos"}: | ||||
|             return kerberos_authenticate | ||||
| @@ -302,14 +187,11 @@ def authenticate(optional=False): | ||||
| def login_required(func): | ||||
|     return authenticate()(func) | ||||
|  | ||||
|  | ||||
| def login_optional(func): | ||||
|     return authenticate(optional=True)(func) | ||||
|  | ||||
|  | ||||
| def authorize_admin(func): | ||||
|  | ||||
|     def whitelist_authorize(resource, req, resp, *args, **kwargs): | ||||
|     def whitelist_authorize_admin(resource, req, resp, *args, **kwargs): | ||||
|         # Check for username whitelist | ||||
|         if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST: | ||||
|             logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", | ||||
| @@ -317,8 +199,13 @@ def authorize_admin(func): | ||||
|             raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user")) | ||||
|         return func(resource, req, resp, *args, **kwargs) | ||||
|  | ||||
|     if config.AUTHORIZATION_BACKEND == "whitelist": | ||||
|         return whitelist_authorize | ||||
|     else: | ||||
|         return member_of(config.ADMINS_GROUP)(func) | ||||
|     def authorize_admin(resource, req, resp, *args, **kwargs): | ||||
|         if req.context.get("user").is_admin(): | ||||
|             req.context["admin_authorized"] = True | ||||
|             return func(resource, req, resp, *args, **kwargs) | ||||
|         logger.info("User '%s' not authorized to access administrative API", req.context.get("user").name) | ||||
|         raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations") | ||||
|  | ||||
|     if config.AUTHORIZATION_BACKEND == "whitelist": | ||||
|         return whitelist_authorize_admin | ||||
|     return authorize_admin | ||||
|   | ||||
| @@ -26,12 +26,12 @@ def publish_certificate(func): | ||||
|         cert = func(csr, *args, **kwargs) | ||||
|         assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) | ||||
|  | ||||
|         if cert.email_address: | ||||
|             mailer.send( | ||||
|                 "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address), | ||||
|                 "certificate-signed.md", | ||||
|                 attachments=(cert,), | ||||
|                 certificate=cert) | ||||
|         mailer.send( | ||||
|             "certificate-signed.md", | ||||
|             to= "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) if | ||||
|                 cert.given_name and cert.surname else cert.email_address, | ||||
|             attachments=(cert,), | ||||
|             certificate=cert) | ||||
|  | ||||
|         if config.PUSH_PUBLISH: | ||||
|             url = config.PUSH_PUBLISH % csr.fingerprint() | ||||
| @@ -85,7 +85,9 @@ def store_request(buf, overwrite=False): | ||||
|             fh.write(buf) | ||||
|         os.rename(request_path + ".part", request_path) | ||||
|  | ||||
|     return Request(open(request_path)) | ||||
|     req = Request(open(request_path)) | ||||
|     mailer.send("request-stored.md", attachments=(req,), request=req) | ||||
|     return req | ||||
|  | ||||
|  | ||||
| def signer_exec(cmd, *bits): | ||||
| @@ -110,6 +112,7 @@ def revoke_certificate(common_name): | ||||
|     revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) | ||||
|     os.rename(cert.path, revoked_filename) | ||||
|     push.publish("certificate-revoked", cert.common_name) | ||||
|     mailer.send("certificate-revoked.md", attachments=(cert,), certificate=cert) | ||||
|  | ||||
|  | ||||
| def list_requests(directory=config.REQUESTS_DIR): | ||||
| @@ -184,7 +187,7 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): | ||||
|         if owner.surname: | ||||
|             csr.get_subject().SN = owner.surname | ||||
|         csr.add_extensions([ | ||||
|             crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)]) | ||||
|             crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail.encode("ascii"))]) | ||||
|  | ||||
|     buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr) | ||||
|  | ||||
|   | ||||
							
								
								
									
										273
									
								
								certidude/cli.py
									
									
									
									
									
								
							
							
						
						
									
										273
									
								
								certidude/cli.py
									
									
									
									
									
								
							| @@ -15,6 +15,7 @@ import subprocess | ||||
| import sys | ||||
| from configparser import ConfigParser | ||||
| from certidude import constants | ||||
| from certidude.helpers import certidude_request_certificate | ||||
| from certidude.common import expand_paths, ip_address, ip_network | ||||
| from datetime import datetime | ||||
| from humanize import naturaltime | ||||
| @@ -63,8 +64,6 @@ if os.getuid() >= 1000: | ||||
| @click.command("spawn", help="Run processes for requesting certificates and configuring services") | ||||
| @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") | ||||
| def certidude_request_spawn(fork): | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|  | ||||
|     clients = ConfigParser() | ||||
|     clients.readfp(open("/etc/certidude/client.conf")) | ||||
|  | ||||
| @@ -80,7 +79,7 @@ def certidude_request_spawn(fork): | ||||
|         os.makedirs(run_dir) | ||||
|  | ||||
|     for server in clients.sections(): | ||||
|         if clients.get(server, "managed") != "true": | ||||
|         if clients.get(server, "trigger") != "interface up": | ||||
|             continue | ||||
|  | ||||
|         pid_path = os.path.join(run_dir, server + ".pid") | ||||
| @@ -115,6 +114,7 @@ def certidude_request_spawn(fork): | ||||
|                     clients.get(server, "request_path"), | ||||
|                     clients.get(server, "certificate_path"), | ||||
|                     clients.get(server, "authority_path"), | ||||
|                     clients.get(server, "revocations_path"), | ||||
|                     socket.gethostname(), | ||||
|                     None, | ||||
|                     autosign=True, | ||||
| @@ -133,9 +133,47 @@ def certidude_request_spawn(fork): | ||||
|             csum = csummer.hexdigest() | ||||
|             uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] | ||||
|  | ||||
|             # Intranet HTTPS handled by PKCS#12 bundle generation, | ||||
|             # so it will not be implemented here | ||||
|  | ||||
|             if services.get(endpoint, "service") == "network-manager/openvpn": | ||||
|                 config = ConfigParser() | ||||
|                 config.add_section("connection") | ||||
|                 config.add_section("vpn") | ||||
|                 config.add_section("ipv4") | ||||
|                 config.add_section("ipv6") | ||||
|  | ||||
|                 config.set("connection", "id", endpoint) | ||||
|                 config.set("connection", "uuid", uuid) | ||||
|                 config.set("connection", "type", "vpn") | ||||
|  | ||||
|                 config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn") | ||||
|                 config.set("vpn", "connection-type", "tls") | ||||
|                 config.set("vpn", "comp-lzo", "yes") | ||||
|                 config.set("vpn", "cert-pass-flags", "0") | ||||
|                 config.set("vpn", "tap-dev", "yes") | ||||
|                 config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate | ||||
|                 config.set("vpn", "remote", services.get(endpoint, "remote")) | ||||
|                 config.set("vpn", "key", clients.get(server, "key_path")) | ||||
|                 config.set("vpn", "cert", clients.get(server, "certificate_path")) | ||||
|                 config.set("vpn", "ca", clients.get(server, "authority_path")) | ||||
|  | ||||
|                 config.set("ipv6", "method", "auto") | ||||
|  | ||||
|                 config.set("ipv4", "method", "auto") | ||||
|                 config.set("ipv4", "never-default", "true") | ||||
|  | ||||
|                 # Prevent creation of files with liberal permissions | ||||
|                 os.umask(0o177) | ||||
|  | ||||
|                 # Write keyfile | ||||
|                 with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as configfile: | ||||
|                     config.write(configfile) | ||||
|                 continue | ||||
|  | ||||
|  | ||||
|             # Set up IPsec via NetworkManager | ||||
|             if services.get(endpoint, "service") == "network-manager/strongswan": | ||||
|  | ||||
|                 config = ConfigParser() | ||||
|                 config.add_section("connection") | ||||
|                 config.add_section("vpn") | ||||
| @@ -146,14 +184,14 @@ def certidude_request_spawn(fork): | ||||
|                 config.set("connection", "type", "vpn") | ||||
|  | ||||
|                 config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan") | ||||
|                 config.set("vpn", "userkey", clients.get(server, "key_path")) | ||||
|                 config.set("vpn", "usercert", clients.get(server, "certificate_path")) | ||||
|                 config.set("vpn", "encap", "no") | ||||
|                 config.set("vpn", "address", services.get(endpoint, "remote")) | ||||
|                 config.set("vpn", "virtual", "yes") | ||||
|                 config.set("vpn", "method", "key") | ||||
|                 config.set("vpn", "certificate", clients.get(server, "authority_path")) | ||||
|                 config.set("vpn", "ipcomp", "no") | ||||
|                 config.set("vpn", "address", services.get(endpoint, "remote")) | ||||
|                 config.set("vpn", "userkey", clients.get(server, "key_path")) | ||||
|                 config.set("vpn", "usercert", clients.get(server, "certificate_path")) | ||||
|                 config.set("vpn", "certificate", clients.get(server, "authority_path")) | ||||
|  | ||||
|                 config.set("ipv4", "method", "auto") | ||||
|  | ||||
| @@ -203,9 +241,7 @@ def certidude_request_spawn(fork): | ||||
|                     os.system("ipsec start") | ||||
|                 continue | ||||
|  | ||||
|  | ||||
|  | ||||
|             # TODO: OpenVPN, Puppet, OpenLDAP, intranet HTTPS, <insert awesomeness here> | ||||
|             # TODO: Puppet, OpenLDAP, <insert awesomeness here> | ||||
|  | ||||
|         os.unlink(pid_path) | ||||
|  | ||||
| @@ -284,9 +320,8 @@ def certidude_signer_spawn(kill, no_interaction): | ||||
|     asyncore.loop() | ||||
|  | ||||
|  | ||||
|  | ||||
| @click.command("client", help="Setup X.509 certificates for application") | ||||
| @click.argument("url") #, help="Certidude authority endpoint URL") | ||||
| @click.argument("server") | ||||
| @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, '%s' by default" % HOSTNAME) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) | ||||
| @@ -301,18 +336,18 @@ def certidude_signer_spawn(kill, no_interaction): | ||||
| @click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt by default" % HOSTNAME) | ||||
| @click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt by default") | ||||
| @click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default") | ||||
| def certidude_setup_client(quiet, **kwargs): | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|     return certidude_request_certificate(**kwargs) | ||||
|  | ||||
|  | ||||
| @click.command("server", help="Set up OpenVPN server") | ||||
| @click.argument("url") | ||||
| @click.argument("server") | ||||
| @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) | ||||
| @click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default") | ||||
| @click.option("--local", "-l", default="127.0.0.1", help="OpenVPN listening address, defaults to 127.0.0.1") | ||||
| @click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces") | ||||
| @click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default") | ||||
| @click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") | ||||
| @click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") | ||||
| @@ -321,15 +356,15 @@ def certidude_setup_client(quiet, **kwargs): | ||||
|     type=click.File(mode="w", atomic=True, lazy=True), | ||||
|     help="OpenVPN configuration file") | ||||
| @click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default") | ||||
| @click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME) | ||||
| @click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) | ||||
| @click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to --directory by default") | ||||
| @click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) | ||||
| @click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) | ||||
| @click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default") | ||||
| @click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default") | ||||
| @click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default") | ||||
| @expand_paths() | ||||
| def certidude_setup_openvpn_server(url, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, local, proto, port): | ||||
| def certidude_setup_openvpn_server(server, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, local, proto, port): | ||||
|     # TODO: Intelligent way of getting last IP address in the subnet | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|     subnet_first = None | ||||
|     subnet_last = None | ||||
|     subnet_second = None | ||||
| @@ -346,15 +381,9 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co | ||||
|         click.echo("use following command to sign on Certidude server instead of web interface:") | ||||
|         click.echo() | ||||
|         click.echo("  certidude sign %s" % common_name) | ||||
|     retval = certidude_request_certificate( | ||||
|         url, | ||||
|         key_path, | ||||
|         request_path, | ||||
|         certificate_path, | ||||
|         authority_path, | ||||
|         common_name, | ||||
|         org_unit, | ||||
|         email_address, | ||||
|     retval = certidude_request_certificate(server, | ||||
|         key_path, request_path, certificate_path, authority_path, revocations_path, | ||||
|         common_name, org_unit, email_address, | ||||
|         key_usage="digitalSignature,keyEncipherment", | ||||
|         extended_key_usage="serverAuth", | ||||
|         wait=True) | ||||
| @@ -378,7 +407,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co | ||||
|  | ||||
|  | ||||
| @click.command("nginx", help="Set up nginx as HTTPS server") | ||||
| @click.argument("url") | ||||
| @click.argument("server") | ||||
| @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @click.option("--tls-config", | ||||
| @@ -392,14 +421,14 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co | ||||
| @click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default") | ||||
| @click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) | ||||
| @click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) | ||||
| @click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default") | ||||
| @click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default") | ||||
| @click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default") | ||||
| @click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off'])) | ||||
| @expand_paths() | ||||
| def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client): | ||||
| def certidude_setup_nginx(server, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client): | ||||
|     # TODO: Intelligent way of getting last IP address in the subnet | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|  | ||||
|     if not os.path.exists(certificate_path): | ||||
|         click.echo("As HTTPS server certificate needs specific key usage extensions please") | ||||
| @@ -407,8 +436,8 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d | ||||
|         click.echo() | ||||
|         click.echo("  certidude sign %s" % common_name) | ||||
|         click.echo() | ||||
|     retval = certidude_request_certificate(url, key_path, request_path, | ||||
|         certificate_path, authority_path, common_name, org_unit, | ||||
|     retval = certidude_request_certificate(server, key_path, request_path, | ||||
|         certificate_path, authority_path, revocations_path, common_name, org_unit, | ||||
|         key_usage="digitalSignature,keyEncipherment", | ||||
|         extended_key_usage="serverAuth", | ||||
|         dns = constants.FQDN, wait=True, bundle=True) | ||||
| @@ -446,33 +475,28 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d | ||||
|  | ||||
|  | ||||
| @click.command("client", help="Set up OpenVPN client") | ||||
| @click.argument("url") | ||||
| @click.argument("server") | ||||
| @click.argument("remote") | ||||
| @click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") | ||||
| @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) | ||||
| @click.option("--email-address", "-m", help="E-mail associated with the request, none by default") | ||||
| @click.option("--config", "-o", | ||||
|     default="/etc/openvpn/client-to-site.conf", | ||||
|     type=click.File(mode="w", atomic=True, lazy=True), | ||||
|     help="OpenVPN configuration file") | ||||
| @click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default") | ||||
| @click.option("--key-path", "-k", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME) | ||||
| @click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME) | ||||
| @click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default") | ||||
| @click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) | ||||
| @click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) | ||||
| @click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default") | ||||
| @click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default") | ||||
| @expand_paths() | ||||
| def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote): | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|     retval = certidude_request_certificate( | ||||
|         url, | ||||
|         key_path, | ||||
|         request_path, | ||||
|         certificate_path, | ||||
|         authority_path, | ||||
|         common_name, | ||||
|         org_unit, | ||||
|         email_address, | ||||
| def certidude_setup_openvpn_client(server, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, proto, remote): | ||||
|  | ||||
|     retval = certidude_request_certificate(server, | ||||
|         key_path, request_path, certificate_path, authority_path, revocations_path, | ||||
|         common_name, org_unit, email_address, | ||||
|         wait=True) | ||||
|  | ||||
|     if retval: | ||||
| @@ -490,7 +514,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ | ||||
|  | ||||
|  | ||||
| @click.command("server", help="Set up strongSwan server") | ||||
| @click.argument("url") | ||||
| @click.argument("server") | ||||
| @click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate") | ||||
| @@ -511,8 +535,9 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_ | ||||
| @click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME) | ||||
| @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") | ||||
| @click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default") | ||||
| @expand_paths() | ||||
| def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, fqdn): | ||||
| def certidude_setup_strongswan_server(server, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, local, fqdn): | ||||
|     if "." not in common_name: | ||||
|         raise ValueError("Hostname has to be fully qualified!") | ||||
|     if not local: | ||||
| @@ -523,19 +548,13 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email | ||||
|         click.echo("use following command to sign on Certidude server instead of web interface:") | ||||
|         click.echo() | ||||
|         click.echo("  certidude sign %s" % common_name) | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|     retval = certidude_request_certificate( | ||||
|         url, | ||||
|         key_path, | ||||
|         request_path, | ||||
|         certificate_path, | ||||
|         authority_path, | ||||
|         common_name, | ||||
|         org_unit, | ||||
|         email_address, | ||||
|         click.echo() | ||||
|  | ||||
|     retval = certidude_request_certificate(server, | ||||
|         key_path, request_path, certificate_path, authority_path, revocations_path, | ||||
|         common_name, org_unit, email_address, | ||||
|         key_usage="digitalSignature,keyEncipherment", | ||||
|         extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2", | ||||
|         ip_address=local, | ||||
|         dns=fqdn, | ||||
|         wait=True) | ||||
|  | ||||
| @@ -555,7 +574,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email | ||||
|  | ||||
|  | ||||
| @click.command("client", help="Set up strongSwan client") | ||||
| @click.argument("url") | ||||
| @click.argument("server") | ||||
| @click.argument("remote") | ||||
| @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @@ -581,18 +600,12 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email | ||||
| @click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME) | ||||
| @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") | ||||
| @click.option("--revocations-path", "-crl", default="crls/ca.pemf", help="Certificate revocation list, ca.crl relative to -d by default") | ||||
| @expand_paths() | ||||
| def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction): | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|     retval = certidude_request_certificate( | ||||
|         url, | ||||
|         key_path, | ||||
|         request_path, | ||||
|         certificate_path, | ||||
|         authority_path, | ||||
|         common_name, | ||||
|         org_unit, | ||||
|         email_address, | ||||
| def certidude_setup_strongswan_client(server, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction): | ||||
|     retval = certidude_request_certificate(server, | ||||
|         key_path, request_path, certificate_path, authority_path, | ||||
|         common_name, org_unit, email_address, | ||||
|         wait=True) | ||||
|  | ||||
|     if retval: | ||||
| @@ -612,8 +625,8 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo | ||||
|  | ||||
|  | ||||
| @click.command("networkmanager", help="Set up strongSwan client via NetworkManager") | ||||
| @click.argument("url") | ||||
| @click.argument("remote") | ||||
| @click.argument("server") # Certidude server | ||||
| @click.argument("remote") # StrongSwan gateway | ||||
| @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) | ||||
| @@ -622,63 +635,71 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo | ||||
| @click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME) | ||||
| @click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default") | ||||
| @click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default") | ||||
| @expand_paths() | ||||
| def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote): | ||||
|     from certidude.helpers import certidude_request_certificate | ||||
|     retval = certidude_request_certificate( | ||||
|         url, | ||||
|         key_path, | ||||
|         request_path, | ||||
|         certificate_path, | ||||
|         authority_path, | ||||
|         common_name, | ||||
|         org_unit, | ||||
|         email_address, | ||||
| def certidude_setup_strongswan_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote): | ||||
|     retval = certidude_request_certificate(server, | ||||
|         key_path, request_path, certificate_path, authority_path, revocations_path, | ||||
|         common_name, org_unit, email_address, | ||||
|         wait=True) | ||||
|  | ||||
|     if retval: | ||||
|         return retval | ||||
|  | ||||
|     csummer = hashlib.sha1() | ||||
|     csummer.update(remote.encode("ascii")) | ||||
|     csum = csummer.hexdigest() | ||||
|     uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] | ||||
|     services = ConfigParser() | ||||
|     if os.path.exists("/etc/certidude/services.conf"): | ||||
|         services.readfp(open("/etc/certidude/services.conf")) | ||||
|  | ||||
|     config = ConfigParser() | ||||
|     config.add_section("connection") | ||||
|     config.add_section("vpn") | ||||
|     config.add_section("ipv4") | ||||
|     endpoint = "IPSec to %s" % remote | ||||
|  | ||||
|     config.set("connection", "id", remote) | ||||
|     config.set("connection", "uuid", uuid) | ||||
|     config.set("connection", "type", "vpn") | ||||
|     config.set("connection", "autoconnect", "true") | ||||
|     if services.has_section(endpoint): | ||||
|         click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint) | ||||
|     else: | ||||
|         click.echo("Section %s added to /etc/certidude/client.conf" % endpoint) | ||||
|         services.add_section(endpoint) | ||||
|         services.set(endpoint, "authority", server) | ||||
|         services.set(endpoint, "remote", remote) | ||||
|         services.set(endpoint, "service", "network-manager/strongswan") | ||||
|         services.write(open("/etc/certidude/services.conf", "w")) | ||||
|  | ||||
|     config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan") | ||||
|     config.set("vpn", "userkey", key_path) | ||||
|     config.set("vpn", "usercert", certificate_path) | ||||
|     config.set("vpn", "encap", "no") | ||||
|     config.set("vpn", "address", remote) | ||||
|     config.set("vpn", "virtual", "yes") | ||||
|     config.set("vpn", "method", "key") | ||||
|     config.set("vpn", "certificate", authority_path) | ||||
|     config.set("vpn", "ipcomp", "no") | ||||
|  | ||||
|     config.set("ipv4", "method", "auto") | ||||
| @click.command("networkmanager", help="Set up OpenVPN client via NetworkManager") | ||||
| @click.argument("server") # Certidude server | ||||
| @click.argument("remote") # OpenVPN gateway | ||||
| @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) | ||||
| @click.option("--org-unit", "-ou", help="Organizational unit") | ||||
| @click.option("--email-address", "-m", help="E-mail associated with the request, none by default") | ||||
| @click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default") | ||||
| @click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME) | ||||
| @click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME) | ||||
| @click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME) | ||||
| @click.option("--authority-path", "-ca", default="ca.crt", help="Certificate path, ca.crt relative to -d by default") | ||||
| @click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default") | ||||
| @expand_paths() | ||||
| def certidude_setup_openvpn_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote): | ||||
|     retval = certidude_request_certificate(server, | ||||
|         key_path, request_path, certificate_path, authority_path, revocations_path, | ||||
|         common_name, org_unit, email_address, | ||||
|         wait=True) | ||||
|  | ||||
|     # Prevent creation of files with liberal permissions | ||||
|     os.umask(0o277) | ||||
|     if retval: | ||||
|         return retval | ||||
|  | ||||
|     # Write keyfile | ||||
|     with open(os.path.join("/etc/NetworkManager/system-connections", remote), "w") as configfile: | ||||
|         config.write(configfile) | ||||
|     services = ConfigParser() | ||||
|     if os.path.exists("/etc/certidude/services.conf"): | ||||
|         services.readfp(open("/etc/certidude/services.conf")) | ||||
|  | ||||
|     # TODO: Avoid race condition here | ||||
|     sleep(3) | ||||
|  | ||||
|     # Tell NetworkManager to bring up the VPN connection | ||||
|     subprocess.call(("nmcli", "c", "up", "uuid", uuid)) | ||||
|     endpoint = "OpenVPN to %s" % remote | ||||
|  | ||||
|     if services.has_section(endpoint): | ||||
|         click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint) | ||||
|     else: | ||||
|         click.echo("Section %s added to /etc/certidude/client.conf" % endpoint) | ||||
|         services.add_section(endpoint) | ||||
|         services.set(endpoint, "authority", server) | ||||
|         services.set(endpoint, "remote", remote) | ||||
|         services.set(endpoint, "service", "network-manager/openvpn") | ||||
|         services.write(open("/etc/certidude/services.conf", "w")) | ||||
|  | ||||
| @click.command("production", help="Set up nginx, uwsgi and cron") | ||||
| @click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") | ||||
| @@ -761,7 +782,8 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw | ||||
| @click.option("--push-server", default="", help="Streaming nginx push server") | ||||
| @click.option("--email-address", default="certidude@" + FQDN, help="E-mail address of the CA") | ||||
| @click.option("--directory", default=os.path.join("/var/lib/certidude", FQDN), help="Directory for authority files, /var/lib/certidude/ by default") | ||||
| def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address): | ||||
| @click.option("--outbox", default="smtp://smtp.%s" % constants.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % constants.DOMAIN) | ||||
| def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address, outbox): | ||||
|  | ||||
|     # Make sure common_name is valid | ||||
|     if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name): | ||||
| @@ -1155,6 +1177,7 @@ certidude_setup_strongswan.add_command(certidude_setup_strongswan_client) | ||||
| certidude_setup_strongswan.add_command(certidude_setup_strongswan_networkmanager) | ||||
| certidude_setup_openvpn.add_command(certidude_setup_openvpn_server) | ||||
| certidude_setup_openvpn.add_command(certidude_setup_openvpn_client) | ||||
| certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager) | ||||
| certidude_setup.add_command(certidude_setup_authority) | ||||
| certidude_setup.add_command(certidude_setup_openvpn) | ||||
| certidude_setup.add_command(certidude_setup_strongswan) | ||||
|   | ||||
| @@ -57,7 +57,6 @@ except configparser.NoOptionError: | ||||
|     PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" | ||||
|     PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" | ||||
|  | ||||
|  | ||||
| TAGGING_BACKEND = cp.get("tagging", "backend") | ||||
| LOGGING_BACKEND = cp.get("logging", "backend") | ||||
| LEASES_BACKEND = cp.get("leases", "backend") | ||||
| @@ -68,18 +67,16 @@ if "whitelist" == AUTHORIZATION_BACKEND: | ||||
|     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") | ||||
|     ADMINS_GROUP = cp.get("authorization", "posix admin group") | ||||
|     ADMIN_GROUP = cp.get("authorization", "posix admin group") | ||||
| elif "ldap" == AUTHORIZATION_BACKEND: | ||||
|     USERS_GROUP = cp.get("authorization", "ldap user group") | ||||
|     ADMINS_GROUP = cp.get("authorization", "ldap admin group") | ||||
|     LDAP_GSSAPI_CRED_CACHE = cp.get("authorization", "ldap gssapi credential cache") | ||||
|     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) | ||||
|  | ||||
| LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") | ||||
| LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter") | ||||
| LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter") | ||||
| LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter") | ||||
|  | ||||
| for line in open("/etc/ldap/ldap.conf"): | ||||
|     line = line.strip().lower() | ||||
|     if "#" in line: | ||||
| @@ -92,6 +89,5 @@ for line in open("/etc/ldap/ldap.conf"): | ||||
|         click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS)) | ||||
|     elif key == "base": | ||||
|         LDAP_BASE = value | ||||
| else: | ||||
|     click.echo("No LDAP servers specified in /etc/ldap/ldap.conf") | ||||
|  | ||||
| # TODO: Check if we don't have base or servers | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
|  | ||||
| import click | ||||
| import socket | ||||
|  | ||||
| FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import re | ||||
| import types | ||||
| from datetime import date, time, datetime | ||||
| from OpenSSL import crypto | ||||
| from certidude.auth import User | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| @@ -76,6 +77,9 @@ class MyEncoder(json.JSONEncoder): | ||||
|         if isinstance(obj, Certificate): | ||||
|             return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \ | ||||
|                 if hasattr(obj, key) and getattr(obj, key)]) | ||||
|         if isinstance(obj, User): | ||||
|             return dict(name=obj.name, given_name=obj.given_name, | ||||
|                 surname=obj.surname, mail=obj.mail) | ||||
|         if hasattr(obj, "serialize"): | ||||
|             return obj.serialize() | ||||
|         return json.JSONEncoder.default(self, obj) | ||||
| @@ -95,16 +99,20 @@ def serialize(func): | ||||
|                 resp.set_header("Content-Type", "application/json") | ||||
|                 resp.set_header("Content-Disposition", "inline") | ||||
|                 resp.body = json.dumps(r, cls=MyEncoder) | ||||
|  | ||||
|             elif hasattr(r, "content_type") and req.client_accepts(r.content_type): | ||||
|                 resp.set_header("Content-Type", r.content_type) | ||||
|                 resp.set_header("Content-Disposition", | ||||
|                     ("attachment; filename=%s" % r.suggested_filename).encode("ascii")) | ||||
|                 resp.body = r.dump() | ||||
|             else: | ||||
|                 logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept)) | ||||
|             elif hasattr(r, "content_type"): | ||||
|                 logger.debug("Client did not accept application/json or %s, " | ||||
|                     "client expected %s", r.content_type, req.accept) | ||||
|                 raise falcon.HTTPUnsupportedMediaType( | ||||
|                     "Client did not accept application/json or %s" % r.content_type) | ||||
|             else: | ||||
|                 logger.debug("Client did not accept application/json, client expected %s", req.accept) | ||||
|                 raise falcon.HTTPUnsupportedMediaType( | ||||
|                     "Client did not accept application/json") | ||||
|         return r | ||||
|     return wrapped | ||||
|  | ||||
|   | ||||
| @@ -2,11 +2,14 @@ | ||||
| import click | ||||
| import os | ||||
| import requests | ||||
| import subprocess | ||||
| import tempfile | ||||
| from certidude import errors | ||||
| from certidude.wrappers import Certificate, Request | ||||
| from configparser import ConfigParser | ||||
| from OpenSSL import crypto | ||||
|  | ||||
| def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False): | ||||
| def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False): | ||||
|     """ | ||||
|     Exchange CSR for certificate using Certidude HTTP API server | ||||
|     """ | ||||
| @@ -18,38 +21,80 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | ||||
|     if wait: | ||||
|         request_params.add("wait=forever") | ||||
|  | ||||
|     # Expand ca.example.com to http://ca.example.com/api/ | ||||
|     if not url.endswith("/"): | ||||
|         url += "/api/" | ||||
|     if "//" not in url: | ||||
|         url = "http://" + url | ||||
|  | ||||
|     authority_url = url + "certificate" | ||||
|     request_url = url + "request" | ||||
|     # Expand ca.example.com | ||||
|     authority_url = "http://%s/api/certificate/" % server | ||||
|     request_url = "http://%s/api/request/" % server | ||||
|     revoked_url = "http://%s/api/revoked/" % server | ||||
|  | ||||
|     if request_params: | ||||
|         request_url = request_url + "?" + "&".join(request_params) | ||||
|  | ||||
|     if os.path.exists(certificate_path): | ||||
|         click.echo("Found certificate: %s" % certificate_path) | ||||
|         # TODO: Check certificate validity, download CRL? | ||||
|         return | ||||
|  | ||||
|     if os.path.exists(authority_path): | ||||
|         click.echo("Found CA certificate in: %s" % authority_path) | ||||
|         click.echo("Found authority certificate in: %s" % authority_path) | ||||
|     else: | ||||
|         click.echo("Attempting to fetch CA certificate from %s" % authority_url) | ||||
|  | ||||
|         click.echo("Attempting to fetch authority certificate from %s" % authority_url) | ||||
|         try: | ||||
|             r = requests.get(authority_url, | ||||
|                     headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"}) | ||||
|             cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) | ||||
|         except crypto.Error: | ||||
|             raise ValueError("Failed to parse PEM: %s" % r.text) | ||||
|         with open(authority_path + ".part", "w") as oh: | ||||
|         authority_partial = tempfile.mktemp(prefix=authority_path + ".part") | ||||
|         with open(authority_partial, "w") as oh: | ||||
|             oh.write(r.text) | ||||
|         click.echo("Writing CA certificate to: %s" % authority_path) | ||||
|         os.rename(authority_path + ".part", authority_path) | ||||
|         click.echo("Writing authority certificate to: %s" % authority_path) | ||||
|         os.rename(authority_partial, authority_path) | ||||
|  | ||||
|     # Fetch certificate revocation list | ||||
|     r = requests.get(revoked_url, stream=True) | ||||
|     click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path)) | ||||
|     revocations_partial = tempfile.mktemp(prefix=revocations_path + ".part") | ||||
|     with open(revocations_partial, 'wb') as f: | ||||
|         for chunk in r.iter_content(chunk_size=8192): | ||||
|             if chunk: | ||||
|                 f.write(chunk) | ||||
|     if subprocess.call(("openssl", "crl", "-CAfile", authority_path, "-in", revocations_partial, "-noout")): | ||||
|         raise ValueError("Failed to verify CRL in %s" % revocations_partial) | ||||
|     else: | ||||
|         # TODO: Check monotonically increasing CRL number | ||||
|         click.echo("Certificate revocation list passed verification") | ||||
|         os.rename(revocations_partial, revocations_path) | ||||
|  | ||||
|     # Check if we have been inserted into CRL | ||||
|     if os.path.exists(certificate_path): | ||||
|         cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate_path).read()) | ||||
|         revocation_list = crypto.load_crl(crypto.FILETYPE_PEM, open(revocations_path).read()) | ||||
|         for revocation in revocation_list.get_revoked(): | ||||
|             if int(revocation.get_serial(), 16) == cert.get_serial_number(): | ||||
|                 if revocation.get_reason() == "Certificate Hold": # TODO: 'Remove From CRL' | ||||
|                     # TODO: Disable service for time being | ||||
|                     click.echo("Certificate put on hold, doing nothing for now") | ||||
|                     break | ||||
|  | ||||
|                 # Disable the client if operation has been ceased or | ||||
|                 # the certificate has been superseded by other | ||||
|                 if revocation.get_reason() in ("Cessation Of Operation", "Superseded"): | ||||
|                     if os.path.exists("/etc/certidude/client.conf"): | ||||
|                         clients.readfp(open("/etc/certidude/client.conf")) | ||||
|                         if clients.has_section(server): | ||||
|                             clients.set(server, "trigger", "operation ceased") | ||||
|                             clients.write(open("/etc/certidude/client.conf", "w")) | ||||
|                             click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf") | ||||
|                     # TODO: Disable related services | ||||
|                 if revocation.get_reason() in ("CA Compromise", "AA Compromise"): | ||||
|                     if os.path.exists(authority_path): | ||||
|                         os.remove(key_path) | ||||
|  | ||||
|                 click.echo("Certificate has been revoked, wiping keys and certificates!") | ||||
|                 if os.path.exists(key_path): | ||||
|                     os.remove(key_path) | ||||
|                 if os.path.exists(request_path): | ||||
|                     os.remove(request_path) | ||||
|                 if os.path.exists(certificate_path): | ||||
|                     os.remove(certificate_path) | ||||
|                 break | ||||
|         else: | ||||
|             click.echo("Certificate does not seem to be revoked. Good!") | ||||
|  | ||||
|     try: | ||||
|         request = Request(open(request_path)) | ||||
| @@ -62,8 +107,9 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | ||||
|         key.generate_key(crypto.TYPE_RSA, 4096) | ||||
|  | ||||
|         # Dump private key | ||||
|         key_partial = tempfile.mktemp(prefix=key_path + ".part") | ||||
|         os.umask(0o077) | ||||
|         with open(key_path + ".part", "wb") as fh: | ||||
|         with open(key_partial, "wb") as fh: | ||||
|             fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) | ||||
|  | ||||
|         # Construct CSR | ||||
| @@ -107,10 +153,38 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | ||||
|             fh.write(request.dump()) | ||||
|  | ||||
|         click.echo("Writing private key to: %s" % key_path) | ||||
|         os.rename(key_path + ".part", key_path) | ||||
|         os.rename(key_partial, key_path) | ||||
|         click.echo("Writing certificate signing request to: %s" % request_path) | ||||
|         os.rename(request_path + ".part", request_path) | ||||
|  | ||||
|     # We have CSR now, save the paths to client.conf so we could: | ||||
|     # Update CRL, renew certificate, maybe something extra? | ||||
|  | ||||
|     if not os.path.exists("/etc/certidude"): | ||||
|         os.makedirs("/etc/certidude") | ||||
|  | ||||
|     clients = ConfigParser() | ||||
|     if os.path.exists("/etc/certidude/client.conf"): | ||||
|         clients.readfp(open("/etc/certidude/client.conf")) | ||||
|  | ||||
|     if clients.has_section(server): | ||||
|         click.echo("Section %s already exists in /etc/certidude/client.conf, not reconfiguring" % server) | ||||
|     else: | ||||
|         clients.add_section(server) | ||||
|         clients.set(server, "trigger", "interface up") | ||||
|         clients.set(server, "key_path", key_path) | ||||
|         clients.set(server, "request_path", request_path) | ||||
|         clients.set(server, "certificate_path", certificate_path) | ||||
|         clients.set(server, "authority_path", authority_path) | ||||
|         clients.set(server, "key_path", key_path) | ||||
|         clients.set(server, "revocations_path", revocations_path) | ||||
|         clients.write(open("/etc/certidude/client.conf", "w")) | ||||
|         click.echo("Section %s added to /etc/certidude/client.conf" % repr(server)) | ||||
|  | ||||
|     if os.path.exists(certificate_path): | ||||
|         click.echo("Found certificate: %s" % certificate_path) | ||||
|         # TODO: Check certificate validity, download CRL? | ||||
|         return | ||||
|  | ||||
|     click.echo("Submitting to %s, waiting for response..." % request_url) | ||||
|     submission = requests.post(request_url, | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
|  | ||||
| import click | ||||
| import os | ||||
| import smtplib | ||||
| from certidude.user import User | ||||
| from markdown import markdown | ||||
| from jinja2 import Environment, PackageLoader | ||||
| from email.mime.multipart import MIMEMultipart | ||||
| @@ -10,14 +12,18 @@ from urllib.parse import urlparse | ||||
|  | ||||
| env = Environment(loader=PackageLoader("certidude", "templates/mail")) | ||||
|  | ||||
| def send(recipients, template, attachments=(), **context): | ||||
| def send(template, to=None, attachments=(), **context): | ||||
|     from certidude import authority, config | ||||
|     if not config.OUTBOX: | ||||
|         # Mailbox disabled, don't send e-mail | ||||
|         return | ||||
|  | ||||
|     if not recipients: | ||||
|         raise ValueError("No e-mail recipients specified!") | ||||
|     recipients = u", ".join([unicode(j) for j in User.objects.filter_admins()]) | ||||
|  | ||||
|     if to: | ||||
|         recipients = to + u", " + recipients | ||||
|  | ||||
|     click.echo("Sending e-mail %s to %s" % (template, recipients)) | ||||
|  | ||||
|     scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX) | ||||
|     scheme = scheme.lower() | ||||
|   | ||||
| @@ -485,14 +485,14 @@ output += "\n    E-mail disabled\n"; | ||||
| ; | ||||
| } | ||||
| output += "</p>\n\n<p>Authenticated users allowed from:\n\n"; | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) { | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets"))) { | ||||
| output += "\n    anywhere\n    </p>\n"; | ||||
| ; | ||||
| } | ||||
| else { | ||||
| output += "\n    </p>\n    <ul>\n        "; | ||||
| frame = frame.push(); | ||||
| var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"); | ||||
| var t_3 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets"); | ||||
| if(t_3) {var t_2 = t_3.length; | ||||
| for(var t_1=0; t_1 < t_3.length; t_1++) { | ||||
| var t_4 = t_3[t_1]; | ||||
| @@ -515,14 +515,14 @@ output += "\n    </ul>\n"; | ||||
| ; | ||||
| } | ||||
| output += "\n\n\n<p>Request submission is allowed from:\n\n"; | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) { | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets"))) { | ||||
| output += "\n    anywhere\n    </p>\n"; | ||||
| ; | ||||
| } | ||||
| else { | ||||
| output += "\n    </p>\n    <ul>\n        "; | ||||
| frame = frame.push(); | ||||
| var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"); | ||||
| var t_7 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets"); | ||||
| if(t_7) {var t_6 = t_7.length; | ||||
| for(var t_5=0; t_5 < t_7.length; t_5++) { | ||||
| var t_8 = t_7[t_5]; | ||||
| @@ -545,7 +545,7 @@ output += "\n    </ul>\n"; | ||||
| ; | ||||
| } | ||||
| output += "\n\n<p>Autosign is allowed from:\n"; | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) { | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"autosign_subnets"))) { | ||||
| output += "\n    anywhere\n    </p>\n"; | ||||
| ; | ||||
| } | ||||
| @@ -575,14 +575,14 @@ output += "\n    </ul>\n"; | ||||
| ; | ||||
| } | ||||
| output += "\n\n<p>Authority administration is allowed from:\n"; | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) { | ||||
| if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets"))) { | ||||
| output += "\n    anywhere\n    </p>\n"; | ||||
| ; | ||||
| } | ||||
| else { | ||||
| output += "\n    <ul>\n        "; | ||||
| frame = frame.push(); | ||||
| var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"); | ||||
| var t_15 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets"); | ||||
| if(t_15) {var t_14 = t_15.length; | ||||
| for(var t_13=0; t_13 < t_15.length; t_13++) { | ||||
| var t_16 = t_15[t_13]; | ||||
| @@ -606,15 +606,11 @@ output += "\n    </ul>\n"; | ||||
| } | ||||
| output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\n"; | ||||
| frame = frame.push(); | ||||
| var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); | ||||
| if(t_19) {var t_17; | ||||
| if(runtime.isArray(t_19)) { | ||||
| var t_18 = t_19.length; | ||||
| for(t_17=0; t_17 < t_19.length; t_17++) { | ||||
| var t_20 = t_19[t_17][0] | ||||
| frame.set("handle", t_19[t_17][0]); | ||||
| var t_21 = t_19[t_17][1] | ||||
| frame.set("full_name", t_19[t_17][1]); | ||||
| var t_19 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_users"); | ||||
| if(t_19) {var t_18 = t_19.length; | ||||
| for(var t_17=0; t_17 < t_19.length; t_17++) { | ||||
| var t_20 = t_19[t_17]; | ||||
| frame.set("user", t_20); | ||||
| frame.set("loop.index", t_17 + 1); | ||||
| frame.set("loop.index0", t_17); | ||||
| frame.set("loop.revindex", t_18 - t_17); | ||||
| @@ -622,32 +618,15 @@ frame.set("loop.revindex0", t_18 - t_17 - 1); | ||||
| frame.set("loop.first", t_17 === 0); | ||||
| frame.set("loop.last", t_17 === t_18 - 1); | ||||
| frame.set("loop.length", t_18); | ||||
| output += "\n    <li>"; | ||||
| output += runtime.suppressValue(t_21, env.opts.autoescape); | ||||
| output += "</li>\n"; | ||||
| output += "\n    <li><a href=\"mailto:"; | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_20),"mail"), env.opts.autoescape); | ||||
| output += "\">"; | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_20),"given_name"), env.opts.autoescape); | ||||
| output += " "; | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_20),"surname"), env.opts.autoescape); | ||||
| output += "</a></li>\n"; | ||||
| ; | ||||
| } | ||||
| } else { | ||||
| t_17 = -1; | ||||
| var t_18 = runtime.keys(t_19).length; | ||||
| for(var t_22 in t_19) { | ||||
| t_17++; | ||||
| var t_23 = t_19[t_22]; | ||||
| frame.set("handle", t_22); | ||||
| frame.set("full_name", t_23); | ||||
| frame.set("loop.index", t_17 + 1); | ||||
| frame.set("loop.index0", t_17); | ||||
| frame.set("loop.revindex", t_18 - t_17); | ||||
| frame.set("loop.revindex0", t_18 - t_17 - 1); | ||||
| frame.set("loop.first", t_17 === 0); | ||||
| frame.set("loop.last", t_17 === t_18 - 1); | ||||
| frame.set("loop.length", t_18); | ||||
| output += "\n    <li>"; | ||||
| output += runtime.suppressValue(t_23, env.opts.autoescape); | ||||
| output += "</li>\n"; | ||||
| ; | ||||
| } | ||||
| } | ||||
| } | ||||
| frame = frame.pop(); | ||||
| output += "\n</ul>\n</section>\n\n"; | ||||
| @@ -658,14 +637,14 @@ output += "\n<p>Here you can renew your certificates</p>\n\n"; | ||||
| ; | ||||
| } | ||||
| output += "\n\n"; | ||||
| var t_24; | ||||
| t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); | ||||
| frame.set("s", t_24, true); | ||||
| var t_21; | ||||
| t_21 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); | ||||
| frame.set("s", t_21, true); | ||||
| if(frame.topLevel) { | ||||
| context.setVariable("s", t_24); | ||||
| context.setVariable("s", t_21); | ||||
| } | ||||
| if(frame.topLevel) { | ||||
| context.addExport("s", t_24); | ||||
| context.addExport("s", t_21); | ||||
| } | ||||
| output += "\n\n\n"; | ||||
| if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { | ||||
| @@ -673,24 +652,24 @@ output += "\n<section id=\"requests\">\n    <h1>Pending requests</h1>\n\n    <p> | ||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape); | ||||
| output += "</pre>\n\n    <ul id=\"pending_requests\">\n        "; | ||||
| frame = frame.push(); | ||||
| var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests"); | ||||
| if(t_27) {var t_26 = t_27.length; | ||||
| for(var t_25=0; t_25 < t_27.length; t_25++) { | ||||
| var t_28 = t_27[t_25]; | ||||
| frame.set("request", t_28); | ||||
| frame.set("loop.index", t_25 + 1); | ||||
| frame.set("loop.index0", t_25); | ||||
| frame.set("loop.revindex", t_26 - t_25); | ||||
| frame.set("loop.revindex0", t_26 - t_25 - 1); | ||||
| frame.set("loop.first", t_25 === 0); | ||||
| frame.set("loop.last", t_25 === t_26 - 1); | ||||
| frame.set("loop.length", t_26); | ||||
| var t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests"); | ||||
| if(t_24) {var t_23 = t_24.length; | ||||
| for(var t_22=0; t_22 < t_24.length; t_22++) { | ||||
| var t_25 = t_24[t_22]; | ||||
| frame.set("request", t_25); | ||||
| frame.set("loop.index", t_22 + 1); | ||||
| frame.set("loop.index0", t_22); | ||||
| frame.set("loop.revindex", t_23 - t_22); | ||||
| frame.set("loop.revindex0", t_23 - t_22 - 1); | ||||
| frame.set("loop.first", t_22 === 0); | ||||
| frame.set("loop.last", t_22 === t_23 - 1); | ||||
| frame.set("loop.length", t_23); | ||||
| output += "\n             "; | ||||
| env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) { | ||||
| if(t_31) { cb(t_31); return; } | ||||
| t_29.render(context.getVariables(), frame, function(t_32,t_30) { | ||||
| if(t_32) { cb(t_32); return; } | ||||
| output += t_30 | ||||
| env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_28,t_26) { | ||||
| if(t_28) { cb(t_28); return; } | ||||
| t_26.render(context.getVariables(), frame, function(t_29,t_27) { | ||||
| if(t_29) { cb(t_29); return; } | ||||
| output += t_27 | ||||
| output += "\n\t    "; | ||||
| })}); | ||||
| } | ||||
| @@ -698,24 +677,24 @@ output += "\n\t    "; | ||||
| frame = frame.pop(); | ||||
| output += "\n        <li class=\"notify\">\n            <p>No certificate signing requests to sign!</p>\n        </li>\n    </ul>\n</section>\n\n<section id=\"signed\">\n    <h1>Signed certificates</h1>\n    <input id=\"search\" type=\"search\" class=\"icon search\">\n    <ul id=\"signed_certificates\">\n        "; | ||||
| frame = frame.push(); | ||||
| var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed"))); | ||||
| if(t_35) {var t_34 = t_35.length; | ||||
| for(var t_33=0; t_33 < t_35.length; t_33++) { | ||||
| var t_36 = t_35[t_33]; | ||||
| frame.set("certificate", t_36); | ||||
| frame.set("loop.index", t_33 + 1); | ||||
| frame.set("loop.index0", t_33); | ||||
| frame.set("loop.revindex", t_34 - t_33); | ||||
| frame.set("loop.revindex0", t_34 - t_33 - 1); | ||||
| frame.set("loop.first", t_33 === 0); | ||||
| frame.set("loop.last", t_33 === t_34 - 1); | ||||
| frame.set("loop.length", t_34); | ||||
| var t_32 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed"))); | ||||
| if(t_32) {var t_31 = t_32.length; | ||||
| for(var t_30=0; t_30 < t_32.length; t_30++) { | ||||
| var t_33 = t_32[t_30]; | ||||
| frame.set("certificate", t_33); | ||||
| frame.set("loop.index", t_30 + 1); | ||||
| frame.set("loop.index0", t_30); | ||||
| frame.set("loop.revindex", t_31 - t_30); | ||||
| frame.set("loop.revindex0", t_31 - t_30 - 1); | ||||
| frame.set("loop.first", t_30 === 0); | ||||
| frame.set("loop.last", t_30 === t_31 - 1); | ||||
| frame.set("loop.length", t_31); | ||||
| output += "\n            "; | ||||
| env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) { | ||||
| if(t_39) { cb(t_39); return; } | ||||
| t_37.render(context.getVariables(), frame, function(t_40,t_38) { | ||||
| if(t_40) { cb(t_40); return; } | ||||
| output += t_38 | ||||
| env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_36,t_34) { | ||||
| if(t_36) { cb(t_36); return; } | ||||
| t_34.render(context.getVariables(), frame, function(t_37,t_35) { | ||||
| if(t_37) { cb(t_37); return; } | ||||
| output += t_35 | ||||
| output += "\n\t    "; | ||||
| })}); | ||||
| } | ||||
| @@ -729,31 +708,31 @@ output += "/certificate/ > session.pem\n    openssl ocsp -issuer session.pem -CA | ||||
| output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape); | ||||
| output += "/ocsp/ -serial 0x\n    </pre>\n    -->\n    <ul>\n        "; | ||||
| frame = frame.push(); | ||||
| var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked"); | ||||
| if(t_43) {var t_42 = t_43.length; | ||||
| for(var t_41=0; t_41 < t_43.length; t_41++) { | ||||
| var t_44 = t_43[t_41]; | ||||
| frame.set("j", t_44); | ||||
| frame.set("loop.index", t_41 + 1); | ||||
| frame.set("loop.index0", t_41); | ||||
| frame.set("loop.revindex", t_42 - t_41); | ||||
| frame.set("loop.revindex0", t_42 - t_41 - 1); | ||||
| frame.set("loop.first", t_41 === 0); | ||||
| frame.set("loop.last", t_41 === t_42 - 1); | ||||
| frame.set("loop.length", t_42); | ||||
| var t_40 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked"); | ||||
| if(t_40) {var t_39 = t_40.length; | ||||
| for(var t_38=0; t_38 < t_40.length; t_38++) { | ||||
| var t_41 = t_40[t_38]; | ||||
| frame.set("j", t_41); | ||||
| frame.set("loop.index", t_38 + 1); | ||||
| frame.set("loop.index0", t_38); | ||||
| frame.set("loop.revindex", t_39 - t_38); | ||||
| frame.set("loop.revindex0", t_39 - t_38 - 1); | ||||
| frame.set("loop.first", t_38 === 0); | ||||
| frame.set("loop.last", t_38 === t_39 - 1); | ||||
| frame.set("loop.length", t_39); | ||||
| output += "\n            <li id=\"certificate_"; | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_44),"sha256sum"), env.opts.autoescape); | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_41),"sha256sum"), env.opts.autoescape); | ||||
| output += "\">\n                "; | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape); | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_41),"changed"), env.opts.autoescape); | ||||
| output += "\n                "; | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape); | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_41),"serial_number"), env.opts.autoescape); | ||||
| output += " <span class=\"monospace\">"; | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape); | ||||
| output += runtime.suppressValue(runtime.memberLookup((t_41),"identity"), env.opts.autoescape); | ||||
| output += "</span>\n            </li>\n        "; | ||||
| ; | ||||
| } | ||||
| } | ||||
| if (!t_42) { | ||||
| if (!t_39) { | ||||
| output += "\n            <li>Great job! No certificate signing requests to sign.</li>\n\t    "; | ||||
| } | ||||
| frame = frame.pop(); | ||||
| @@ -1098,7 +1077,7 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook | ||||
| output += "</div>\n    "; | ||||
| })}); | ||||
| } | ||||
| output += "\n    \n    "; | ||||
| output += "\n\n    "; | ||||
| if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) { | ||||
| output += "\n    <div class=\"person\">"; | ||||
| env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) { | ||||
|   | ||||
| @@ -31,13 +31,13 @@ as such require complete reset of X509 infrastructure if some of them needs to b | ||||
|  | ||||
| <p>Authenticated users allowed from: | ||||
|  | ||||
| {% if "0.0.0.0/0" in session.user_subnets %} | ||||
| {% if "0.0.0.0/0" in session.authority.user_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
|     </p> | ||||
|     <ul> | ||||
|         {% for i in session.user_subnets %} | ||||
|         {% for i in session.authority.user_subnets %} | ||||
|             <li>{{ i }}</li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| @@ -46,20 +46,20 @@ as such require complete reset of X509 infrastructure if some of them needs to b | ||||
|  | ||||
| <p>Request submission is allowed from: | ||||
|  | ||||
| {% if "0.0.0.0/0" in session.request_subnets %} | ||||
| {% if "0.0.0.0/0" in session.authority.request_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
|     </p> | ||||
|     <ul> | ||||
|         {% for subnet in session.request_subnets %} | ||||
|         {% for subnet in session.authority.request_subnets %} | ||||
|             <li>{{ subnet }}</li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| {% endif %} | ||||
|  | ||||
| <p>Autosign is allowed from: | ||||
| {% if "0.0.0.0/0" in session.autosign_subnets %} | ||||
| {% if "0.0.0.0/0" in session.authority.autosign_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
| @@ -72,12 +72,12 @@ as such require complete reset of X509 infrastructure if some of them needs to b | ||||
| {% endif %} | ||||
|  | ||||
| <p>Authority administration is allowed from: | ||||
| {% if "0.0.0.0/0" in session.admin_subnets %} | ||||
| {% if "0.0.0.0/0" in session.authority.admin_subnets %} | ||||
|     anywhere | ||||
|     </p> | ||||
| {% else %} | ||||
|     <ul> | ||||
|         {% for subnet in session.admin_subnets %} | ||||
|         {% for subnet in session.authority.admin_subnets %} | ||||
|             <li>{{ subnet }}</li> | ||||
|         {% endfor %} | ||||
|     </ul> | ||||
| @@ -86,8 +86,8 @@ as such require complete reset of X509 infrastructure if some of them needs to b | ||||
| <p>Authority administration allowed for:</p> | ||||
|  | ||||
| <ul> | ||||
| {% for handle, full_name in session.admin_users %} | ||||
|     <li>{{ full_name }}</li> | ||||
| {% for user in session.authority.admin_users %} | ||||
|     <li><a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a></li> | ||||
| {% endfor %} | ||||
| </ul> | ||||
| </section> | ||||
|   | ||||
| @@ -57,4 +57,4 @@ certificate path = {{ ca_crt }} | ||||
| requests dir = {{ directory }}/requests/ | ||||
| signed dir = {{ directory }}/signed/ | ||||
| revoked dir = {{ directory }}/revoked/ | ||||
| outbox = smtp://localhost | ||||
| outbox = {{ outbox }} | ||||
|   | ||||
							
								
								
									
										6
									
								
								certidude/templates/mail/certificate-revoked.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								certidude/templates/mail/certificate-revoked.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| Certificate {{certificate.common_name}} ({{certificate.serial_number}}) revoked | ||||
|  | ||||
| This is simply to notify that certificate {{ certificate.common_name }} | ||||
| was revoked. | ||||
|  | ||||
| Services making use of this certificates might become unavailable. | ||||
							
								
								
									
										5
									
								
								certidude/templates/mail/request-stored.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								certidude/templates/mail/request-stored.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| Certificate signing request {{request.common_name}} stored | ||||
|  | ||||
| This is simply to notify that certificate signing request for {{ request.common_name }} | ||||
| was stored. You may log in with a certificate authority administration account to sign it. | ||||
|  | ||||
| @@ -15,6 +15,7 @@ server { | ||||
|     ssl_certificate {{certificate_path}}; | ||||
|     ssl_certificate_key {{key_path}}; | ||||
|     ssl_client_certificate {{authority_path}}; | ||||
|     ssl_crl {{revocations_path}}; | ||||
|     ssl_verify_client {{verify_client}}; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -7,9 +7,6 @@ events { | ||||
| } | ||||
|  | ||||
| http { | ||||
|     {% if not push_server %} | ||||
|     push_stream_shared_memory_size 32M; | ||||
|     {% endif %} | ||||
|     include mime.types; | ||||
|     default_type application/octet-stream; | ||||
|     sendfile on; | ||||
| @@ -21,7 +18,7 @@ http { | ||||
|     } | ||||
|  | ||||
|     server { | ||||
|         server_name {{hostname}}; | ||||
|         server_name {{hostname}}; # TODO: FQDN, SSL | ||||
|         listen 80 default_server; | ||||
|         listen [::]:80 default_server ipv6only=on; | ||||
|         error_page 500 502 503 504 /50x.html; | ||||
|   | ||||
| @@ -2,7 +2,7 @@ client | ||||
| remote {{remote}} | ||||
| remote-cert-tls server | ||||
| proto {{proto}} | ||||
| dev tap0 | ||||
| dev tap | ||||
| nobind | ||||
| key {{key_path}} | ||||
| cert {{certificate_path}} | ||||
| @@ -10,4 +10,5 @@ ca {{authority_path}} | ||||
| comp-lzo | ||||
| user nobody | ||||
| group nogroup | ||||
|  | ||||
| persist-tun | ||||
| persist-key | ||||
|   | ||||
| @@ -2,15 +2,18 @@ mode server | ||||
| tls-server | ||||
| proto {{proto}} | ||||
| port {{port}} | ||||
| dev tap0 | ||||
| dev tap | ||||
| local {{local}} | ||||
| key {{key_path}} | ||||
| cert {{certificate_path}} | ||||
| ca {{authority_path}} | ||||
| crl-verify {{revocations_path}} | ||||
| dh {{dhparam_path}} | ||||
| comp-lzo | ||||
| user nobody | ||||
| group nogroup | ||||
| persist-tun | ||||
| persist-key | ||||
| ifconfig-pool-persist /tmp/openvpn-leases.txt | ||||
| ifconfig {{subnet_first}} {{subnet.netmask}} | ||||
| server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}} | ||||
|   | ||||
| @@ -5,7 +5,7 @@ processes = 1 | ||||
| vacuum = true | ||||
| uid = {{username}} | ||||
| gid = {{username}} | ||||
| plugins = python34 | ||||
| plugins = python27 | ||||
| chdir = /tmp | ||||
| module = certidude.wsgi | ||||
| callable = app | ||||
|   | ||||
							
								
								
									
										165
									
								
								certidude/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								certidude/user.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
|  | ||||
| import click | ||||
| import grp | ||||
| import ldap | ||||
| import ldap.sasl | ||||
| import os | ||||
| import pwd | ||||
| from certidude import constants, config | ||||
|  | ||||
| class User(object): | ||||
|     def __init__(self, username, mail, given_name="", surname=""): | ||||
|         if "@" not in mail: | ||||
|             raise ValueError("Invalid e-mail %s" % repr(mail)) | ||||
|         self.name = username | ||||
|         self.mail = mail | ||||
|         self.given_name = given_name | ||||
|         self.surname = surname | ||||
|  | ||||
|     def __unicode__(self): | ||||
|         if self.given_name and self.surname: | ||||
|             return u"%s %s <%s>" % (self.given_name, self.surname, self.mail) | ||||
|         else: | ||||
|             return self.mail | ||||
|  | ||||
|     def __hash__(self): | ||||
|         return hash(self.mail) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return self.mail == other.mail | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return unicode(self).encode("utf-8") | ||||
|  | ||||
|     def is_admin(self): | ||||
|         if not hasattr(self, "_is_admin"): | ||||
|             self._is_admin = self.objects.is_admin(self) | ||||
|         return self._is_admin | ||||
|  | ||||
|     class DoesNotExist(StandardError): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| class PosixUserManager(object): | ||||
|     def get(self, username): | ||||
|         _, _, _, _, gecos, _, _ = pwd.getpwnam(username) | ||||
|         gecos = gecos.decode("utf-8").split(",") | ||||
|         full_name = gecos[0] | ||||
|         mail = username + "@" + constants.DOMAIN | ||||
|         if full_name and " " in full_name: | ||||
|             given_name, surname = full_name.split(" ", 1) | ||||
|             return User(username, mail, given_name, surname) | ||||
|         return User(username, mail) | ||||
|  | ||||
|     def filter_admins(self): | ||||
|         _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP) | ||||
|         for username in members: | ||||
|             yield self.get(username) | ||||
|  | ||||
|     def is_admin(self, username): | ||||
|         import grp | ||||
|         _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP) | ||||
|         return username in members | ||||
|  | ||||
|  | ||||
| class DirectoryConnection(object): | ||||
|     def __enter__(self): | ||||
|         # TODO: Implement simple bind | ||||
|         if not os.path.exists(config.LDAP_GSSAPI_CRED_CACHE): | ||||
|             raise ValueError("Ticket cache not initialized, unable to " | ||||
|                 "authenticate with computer account against LDAP server!") | ||||
|         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!") | ||||
|  | ||||
|     def __exit__(self, type, value, traceback): | ||||
|         self.conn.unbind_s | ||||
|  | ||||
|  | ||||
| class ActiveDirectoryUserManager(object): | ||||
|     def get(self, username): | ||||
|         # TODO: Sanitize username | ||||
|         if "@" in username: | ||||
|             username, _ = username.split("@", 1) | ||||
|         with DirectoryConnection() as conn: | ||||
|             ft = config.LDAP_USER_FILTER % username | ||||
|             attribs = "cn", "givenName", "sn", "mail", "userPrincipalName" | ||||
|             r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, | ||||
|                 ft.encode("utf-8"), attribs) | ||||
|             for dn, entry in r: | ||||
|                 if not dn: | ||||
|                     continue | ||||
|                 if entry.get("givenname") and entry.get("sn"): | ||||
|                     given_name, = entry.get("givenName") | ||||
|                     surname, = entry.get("sn") | ||||
|                 else: | ||||
|                     cn, = entry.get("cn") | ||||
|                     if " " in cn: | ||||
|                         given_name, surname = cn.split(" ", 1) | ||||
|                     else: | ||||
|                         given_name, surname = cn, "" | ||||
|  | ||||
|                 mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,) | ||||
|                 return User(username.decode("utf-8"), mail.decode("utf-8"), | ||||
|                     given_name.decode("utf-8"), surname.decode("utf-8")) | ||||
|             raise User.DoesNotExist("User %s does not exist" % username) | ||||
|  | ||||
|     def filter(self, ft): | ||||
|         with DirectoryConnection() as conn: | ||||
|             attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName" | ||||
|             r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, | ||||
|                 ft.encode("utf-8"), attribs) | ||||
|             for dn,entry in r: | ||||
|                 if not dn: | ||||
|                     continue | ||||
|                 username, = entry.get("sAMAccountName") | ||||
|                 cn, = entry.get("cn") | ||||
|                 mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,) | ||||
|                 if entry.get("givenName") and entry.get("sn"): | ||||
|                     given_name, = entry.get("givenName") | ||||
|                     surname, = entry.get("sn") | ||||
|                 else: | ||||
|                     cn, = entry.get("cn") | ||||
|                     if " " in cn: | ||||
|                         given_name, surname = cn.split(" ", 1) | ||||
|                     else: | ||||
|                         given_name, surname = cn, "" | ||||
|                 yield User(username.decode("utf-8"), mail.decode("utf-8"), | ||||
|                     given_name.decode("utf-8"), surname.decode("utf-8")) | ||||
|  | ||||
|     def filter_admins(self): | ||||
|         """ | ||||
|         Return admin User objects | ||||
|         """ | ||||
|         return self.filter(config.LDAP_ADMIN_FILTER % "*") | ||||
|  | ||||
|     def all(self): | ||||
|         """ | ||||
|         Return all valid User objects | ||||
|         """ | ||||
|         return self.filter(ft=config.LDAP_USER_FILTER % "*") | ||||
|  | ||||
|     def is_admin(self, user): | ||||
|         with DirectoryConnection() as conn: | ||||
|             ft = config.LDAP_ADMIN_FILTER % user.name | ||||
|             r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE, | ||||
|                 ft.encode("utf-8"), ["cn"]) | ||||
|             for dn, entry in r: | ||||
|                 if not dn: | ||||
|                     continue | ||||
|                 return True | ||||
|             return False | ||||
|  | ||||
| if config.ACCOUNTS_BACKEND == "ldap": | ||||
|     User.objects = ActiveDirectoryUserManager() | ||||
| elif config.ACCOUNTS_BACKEND == "posix": | ||||
|     User.objects = PosixUserManager() | ||||
| else: | ||||
|     raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND) | ||||
|  | ||||
| @@ -9,7 +9,6 @@ ipaddress==1.0.16 | ||||
| ipsecparse==0.1.0 | ||||
| Jinja2==2.8 | ||||
| Markdown==2.6.5 | ||||
| MarkupSafe==0.23 | ||||
| pyasn1==0.1.8 | ||||
| pycrypto==2.6.1 | ||||
| pykerberos==1.1.8 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user